本文转自公众号“达达京东到家技术”,已获授权转载 在分布式系统中,经常需要对大量的数据、消息、http 请求等进行唯一标识,例如:对于分布式系统,服务间相互调用需要唯一标识,调用链路分析的时候需要使用这个唯一标识。这个时候数据库自增主键已经不能满足需求,需要一个能够生成全局唯一 ID 的系统,这个系统需要满足以下需求:
UUID 是 Universally Unique Identifier 的缩写,它是指在一定范围内 (从特定的名字空间到全球) 唯一的机器生成的标识符,UUID 是 32位的16 进制数字,长为 128 位,例如:3F2504E0-4F89-11D3-9A0C-0305E82C3301。 UUID 经由一定的算法机器生成,为了保证 UUID 的唯一性,规范定义了包括网卡 MAC 地址、时间戳、名字空间 (Namespace)、随机或伪随机数、时序等元素,以及从这些元素生成 UUID 的算法。UUID 的复杂特性在保证了其唯一性的同时,意味着只能由计算机生成。 优点:
缺点:
这个方案是由 Flickr 团队提出,主要思路采用了 MySQL 自增长 ID 的机制 (auto_increment + replace into) replace into 跟 insert 功能类似,不同点在于:replace into 首先尝试插入数据到表中,如果发现表中已经有此行数据 (根据主键或者唯 - 索引判断) 则先删除此行数据,然后插入新的数据, 否则直接插入新数据。 为了避免单点故障,最少需要两个数据库实例,通过区分 auto_increment 的起始值和步长来生成奇偶数的 ID。 优点:
缺点:
对于依赖 MySQL 性能问题,可用如下方案解决: 在分布式环境中我们可以部署多台机器,每台设置不同的初始值,并且步长为机器台数,比如部署 N 台,每台的初始值就为 0,1,2,3...N-1,步长为 N。 以上方案虽然解决了性能问题,但是也存在很大的局限性:
这种方案生成一个 64bit 的数字,64bit 被划分成多个段,分别表示时间戳、机器编码、序号。 ID为64bit 的long 数字,由三部分组成:
优点:
缺点:依赖机器时钟,如果机器时钟回拨,会导致重复ID生成。 在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,有时候会出现不是全局递增的情况。 TDDL 是阿里的分库分表中间件,它里面包含了全局数据库 ID 的生成方式,主要思路:
优点:
缺点:
综合对比以上四种实现方案,以及我们的业务需求,最后决定采用第三种方案。 主要原因:
依据实际业务需求和系统规划,对算法进行局部调整,实现了发号器 snowflake 方案。 发号器 snowflake 方案中对 bit 的划分做了如下调整:
机器编码维护: 机器编码是不同机器之间产生唯一 ID 的重要依据,不能重复,一旦重复,就会导致有相同机器编码的服务器生成的 ID 大量重复。 如果部署的机器只是少量的,可以人工维护,如果大量,手动维护成本高,考虑到自动部署、运维等等问题,机器编码最好由系统自动维护,有以下两个方案可供选择:
这里我们使用 ZooKeeper 持久顺序节点特性来配置维护 WORKID发号器的启动顺序如下:
一旦取回 WORKID,缓存在本地文件中,后续直接使用,不再与 ZooKeeper 进行任何交互,此方案对 ZooKeeper 依赖极小。 时钟问题: snowflake方案依赖系统时钟,如果机器时钟回拨,就有可能生成重复ID,为了保证ID唯一性,必须解决时钟回拨问题。 可以采取以下几种方案解决时钟问题:
闰秒处理: 闰秒,是指为保持协调世界时接近于世界时时刻,由国际计量局统一规定在年底或年中(也可能在季末)对协调世界时增加或减少1秒的调整。由于地球自转的不均匀性和长期变慢性(主要由潮汐摩擦引起的),会使世界时(民用时)和原子时之间相差超过到±0.9秒时,就把协调世界时向前拨1秒(负闰秒,最后一分钟为59秒)或向后拨1秒(正闰秒,最后一分钟为61秒),闰秒一般加在公历年末或公历六月末。 在闰秒产生的时候系统会出现秒级时间调整,下面我们来分析闰秒对发号器的影响:
部署结构为了实现高可用,避免单点故障,系统部署采用集群水平部署,前置使用Nginx做负载均衡,发号器使用Spring Boot框架,web服务器使用Spring Boot内嵌Tomcat, 发号器和Nginx之间进行心跳检测。 Tomcat调优使用APR Tomcat支持三种接收请求的处理方式:BIO、NIO、APR, 性能上 BIO<><>在Spring Boot程序中增加ARP配置开启APR(这里有一个配置变量来控制是否开启) 整个开发过程都非常顺利,测试的时候tps也很高,心情很愉快,世界很美好,突然一个意外出现,发现存在full gc现象,有内存溢出? 于是分析了好几遍程序,也没找到明显的线索,只能开始jvm调试旅程。 pingpoint 监控图: (上图中红色部署表示full gc) JVM调试最直接的就是获取full gc时的jvm dump文件,以及gc log进行分析: 为了获取dump文件,在jvm参数中加上: 参数介绍: 配置上面的虚拟机参数后,虚拟机gc的时候会把gc相关信息输出到文件gc.log中,full gc前后,会生成当时虚拟机的内存dump文件。从pingpoint监控图中可以看出full gc是发生在持久区域。 使用jmap 工具,获取JVM堆内存信息如下: jmap -heap pid 从上图可以看出,使用的堆内存很少,总的堆内存只有0.84% 使用,其它使用指标也都在正常范围,系统装载的类也不多,没有内存泄露。 继续分析gc log: 从gc log 中寻找线索: 这里发现了以下线索:
两次full gc原因都是 Metadata GC Threshold类型,说明pingpoint监控到的full gc是元空间引发的full gc,并非内存泄露引起,但是这个值才34m,距离最大值1081m,还有很大空间,为什么会full gc? 经过查阅官方资料,发现MetaspaceSize的默认大小是21807104b,也就是21296k,而发生GC的时候,元空间已经使用了34722K,从而产生full gc。 方法区: 方法区也是所有线程共享。主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap,字符串常量转移到了java heap,类的静态变量(class statics)转移到了java heap。 在JDK8中,classe metadata(the virtual machines internal presentation of Java class),被存储在叫做Metaspace的native memory。一些新的flags被加入:-XX:MetaspaceSize,class metadata的初始空间配额,以bytes为单位,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当的降低该值;如果释放了很少的空间,就会在不超过MaxMetaspaceSize(如果设置了的话)的情况下,适当的提高该值。 在虚拟机参数中增加MetaspaceSize初始化大小,-XX:MetaspaceSize=128m,重新启动项目,不再有full gc出现。 发号器-达达分布式ID生成系统,以snowflake算法为基础,实现了生成全局唯一ID的功能,解决了在分布式系统唯一ID生成问题。在实现高可用性方面,采用水平集群部署、心跳检测等方案为系统保驾护航。该系统目前已在达达商城等项目中使用。 段同海,就职于达达基础架构团队,主要参与达达分布式 ID 生成系统,日志采集系统等中间件研发工作。 根据 Gartner 的预测,AI 在 2018 年已经不是遥不可及的东西,每家公司都可以碰得到。那么,2018 年,你是否已经做好准备转战 AI 了?应该去哪里学习现成的落地案例和实践经验呢? InfoQ 中国团队为大家梳理了目前机器学习领域的最新动态,并邀请到了来自 Amazon、Snapchat、Etsy、BAT、360、京东等公司 AI 技术负责人前来分享他们的机器学习落地实践经验,部分精彩案例如下:
|
|