
暂无个人介绍
搞了好多年的RPC框架有点疲了,最近在换到中间件的另一个方向MQ,准备做些kafka上阿里云的商业化工作,趁着热乎劲整理下kafka的知识,主要内容是阅读Kafka的作者之一Neha的书:The Definitive Guide的记录,加入一些其他参考。 先说下Kafka名称来历, Kafka作者之一Jay是这么解释的:“我想既然 Kafka 是为了写数据而产生的,那么用作家的名字来命名会显得更有意义。我在大学时期上过很多文学课程,很喜欢 Franz Kafka。况且,对于开源项目来说,这个名字听起来很酷。因此,名字和应用本身基本没有太多联系。” 基本概念 消息与批次:消息相当于数据的一条记录;“在时间延迟和吞吐量之间作出权衡:批次越大,单位时间内处理的消息就越多,单个消息的传输时间就越长。批次数据会被压缩,这样可以提升数据的传输和存储能力,但要做更多的计算处理。” 模式 (schema):像 JSON 和 XML 这些简单的系统,不仅易用,而且可读性好。不过,它们缺乏强类型处理能力,不同版本之间的兼容性也不是很好。Kafka推荐Avro,因为当Avro schema发生变化时,不需要重新生成代码;它还支持强类型和模式进化,其版本既向前兼容,也向后兼容。数据格式的一致性对于 Kafka 来说很重要,来消除了消息的生产者与消费者操作之间的耦合性,通过定义良好的模式,并把它们存放在公共仓库,可以方便我们理解 Kafka 的消息结构。 主题(Topic)与Partition:Kafka 的消息通过主题 进行分类。主题就好比数据库的DataSource数据源,或者文件系统里的文件夹。主题可以被分为若干个分区 ,一个分区就是一个commitlog。跟数据库的分库分表来做比较,一个10库10表的数据源就相当于一个topic分布到10个broker而每个broker由10个partition。生产者在不停地put/insert数据,通过消息键和分区器来实现的,分区器为键生成一个散列值,并将其映射到指定的partition上。消费者在不停地get数据,通过负载均衡策略服务来选择partition进行消费。与大家熟悉的分布分表组件组件做个类比,就能更好更容易的理解在单元化/LDC/自适应多活架构中消息的设计实现;甚至于更容易接受Confluent(Kafka背后的商业化公司,估值挺高)近来一直宣导的“turning-the-database-inside-out”;说到这里,想到了OceanBase对于使用者已经屏蔽了类似TDDL分库分表的组件逻辑,只需要MySQL的客户端就能访问OceanBase,Kafka也是一样,OSS一样,所有的分布式系统都有可类比的地方,只是所针对的场景是不一样的,kafka侧重在queue数据管道,OceanBase侧重在ACID的k/v,OSS侧重在海量文件(大对象)的持久。 Broker与集群:根据特定的硬件及其性能特征,单个 broker 可以轻松处理数千个分区以及每秒百万级的消息量。Kafka 的消息复制机制只能在单个集群里进行,不能在多个集群(集群建议是数据中心内收敛独立部署)之间进行。Kafka 提供了一个叫作 MirrorMaker 的工具,可以用它来实现集群间的消息复制。 Controller:每个 Kafka 集群都有一个控制器controller,负责管理topic分区和副本的状态的变化,以及执行重分配分区之类的管理任务。它是运行在集群某个 broker 上的一个线程。控制器负责看管集群的操作,有时候需要将控制器从一个 broker 迁移到另一个 broker 上。当前控制器将自己注册到 Zookeeper 的一个节点上,这个节点处于集群路径的最顶层,名字叫作 /controller 。手动删除这个节点会释放当前控制器,集群将会进行新的控制器选举。Controller提供的服务功能基本上都是结合ZK的变化通知来一起完成,比如topic的创建或删除。 Coordinator:有两个主要用途,分别是协调consumer之间的partition路由、offset等,协调kafka所定义跨分区跨topic跨消费生产的transaction语义。这块应该是kafka中最复杂的一块。 技术点记录 部署运维方面 如何选择partation的数量 主题需要达到多大的写入吞吐量?例如,是希望每秒钟写入 100KB 还是 1GB ? 从单个分区读取数据的最大吞吐量是多少?每个分区一般都会有一个消费者,如果你知道消费者将数据写入数据库的速度不会超过每秒 50MB,那么你也该知道,从一个分区读取数据的吞吐量不需要超过每秒 50MB。 可以通过类似的方法估算生产者向单个分区写入数据的吞吐量,不过生产者的速度一般比消费者快得多,所以最好为生产者多估算一些吞吐量。 每个 broker 包含的分区个数、可用的磁盘空间和网络带宽。 如果消息是按照不同的键来写入分区的,那么为已有的主题新增分区就会很困难。 单个 broker 对分区个数是有限制的,因为分区越多,占用的内存越多,完成首领选举需要的时间也越长。 根据以上的因素,如果你估算出topic的生产者吞吐量和单个消费者吞吐量,可以用主题吞吐量除以单个消费者吞吐量算出分区的个数。也就是说,如果每秒钟要从主题上写入和读取 1GB 的数据,并且每个消费者每秒钟可以处理 50MB 的数据,那么至少需要 20 个分区。这样就可以让 20 个消费者同时读取这些分区,从而达到每秒钟 1GB 的吞吐量。如果不知道这些信息,那么根据经验,把分区的大小限制在 25GB 以内可以得到比较理想的效果。 消息文件的若干配置 log.retention.ms:通常根据时间来决定数据可以被保留多久,是相对于最后的修改时间,一般来说,最后修改时间指的就是日志片段的关闭时间,也就是文件里最后一个消息的时间戳,移动或者通过Linux命令改动日志文件都可能会导致滚动删除的失效。 log.retention.bytes:另一种方式是通过保留的消息字节数来判断消息是否过期,是作用在每一个分区上。如果同时指定了 log.retention.bytes 和 log.retention.ms (或者另一个时间单位),只要任意一个条件得到满足,消息就会被删除,即是“或”的关系。 log.segment.bytes:当日志片段大小达到 log.segment.bytes 指定的上限(默认是 1GB)时,当前日志片段就会被关闭,一个新的日志片段被打开。如果一个日志片段被关闭,就开始等待过期。这个参数的值越小,就会越频繁地关闭和分配新文件,从而降低磁盘写入的整体效率。对于使用时间戳获取偏移量的操作来说,日志片段越小,结果越准确。 log.segment.ms:指定了多长时间之后日志片段会被关闭。log.segment.bytes 和 log.retention.ms 这两个参数之间也不存在互斥问题,日志片段会在大小或时间达到上限时被关闭,就看哪个条件先得到满足。默认情况下,log.segment.ms 没有设定值,所以只根据大小来关闭日志片段。需要考虑并行关闭多个日志segment的问题。 message.max.bytes:来限制单个消息的大小,默认值是 1 000 000,也就是 1MB。如果生产者尝试发送的消息超过这个大小,不仅消息不会被接收,还会收到 broker 返回的错误信息。这个值对性能有显著的影响。值越大,那么负责处理网络连接和请求的线程就需要花越多的时间来处理这些请求。它还会增加磁盘写入块的大小,从而影响 IO 吞吐量。另外,这个值与消费者客户端设置fetch.message.max.bytes 必须与服务器端设置的消息大小进行协调,如果前者比后者大,就会消费阻塞——这一点我不是很明白,为什么不拉取下一条? 硬件的选择 如果关注整体性能,就需要在预算范围内选择最优化的硬件配置:磁盘吞吐量和容量、内存、网络和 CPU。 硬盘:HDD还是SSD:成本和存储容量的权衡,最终会影响到生产者的RT。 内存:pagecache的大小问题,最终会影响消费者的RT。 网络:因为消费与生产的比重一般大于1,网络流出大于流入的不平衡,同时考虑复制和mirrormake等也会加重不平衡。 CPU:要求较低。主要消耗在安全,解压缩等。 要根据 Kafka 的性能优先级来选择合适的实例。可以先从要保留数据的大小开始考虑,然后考虑生产者方面的性能。如果要求低延迟,那么就需要专门为 I/O 优化过的使用固态硬盘的实例,否则,使用配备了临时存储的实例就可以了。选好存储类型之后,再选择 CPU 和内存就容易得多。实际上,如果使用 AWS,一般会选择 m4 实例或 r3 实例。m4 实例允许较长时间地保留数据,不过磁盘吞吐量会小一些,因为它使用的是弹性块存储。r3 实例使用固态硬盘,具有较高的吞吐量,但保留的数据量会有所限制。如果想两者兼顾,那么需要升级成 i2 实例或 d2 实例,不过它们的成本要高得多。 —— 阿里云对于Kafka的用户也应该有对应的建议配置;从Confluent的Kafka云服务来看,是集成了AWS,Azure,GoogleCloud的部署入口,当前是没有aliyun的选项。 通常来说,在做kafka集群的容量规划时,消息的总容量(包含备份复制)和网络流量情况两个因素就能估算出Broker集群的大小。 OS+JVM的配置 虚拟内存:尽量避免内存交换。内存页和磁盘之间的交换对 Kafka 各方面的性能都有重大影响。建议把 vm.swappiness 参数的值设置得小一点,比如 1。该参数指明了虚拟机的子系统将如何使用交换分区,而不是只把内存页从页面缓存里移除。要优先考虑减小页面缓存,而不是进行内存交换。通过设置 vm.dirty_ratio 参数可以增加被内核进程刷新到磁盘之前的脏页数量,可以将它设为大于 20 的值(这也是系统内存的百分比)。这个值可设置的范围很广,60~80 是个比较合理的区间。不过调整这个参数会带来一些风险,包括未刷新磁盘操作的数量和同步刷新引起的长时间 I/O 等待。如果该参数设置了较高的值,建议启用 Kafka 的复制功能,避免因系统崩溃造成数据丢失。 文件系统:XFS相比较EXT4 为 Kafka 提供了更好的性能,除了由文件系统提供的自动调优之外,无需额外的调优。批量磁盘写入具有更高的效率,可以提升整体的 I/O 吞吐量。不管使用哪一种文件系统来存储日志片段,最好要对挂载点的 noatime 参数进行合理的设置。文件元数据包含 3 个时间戳:创建时间(ctime)、最后修改时间(mtime)以及最后访问时间(atime)。默认情况下,每次文件被读取后都会更新 atime,这会导致大量的磁盘写操作,而且 atime 属性对于Kafka用处不大,可以禁用。 网络:设置 TCP socket 的读写缓冲区,它们的参数分别是 net.ipv4.tcp_wmem 和 net.ipv4.tcp_rmem 。把 net.ipv4.tcp_window_scaling 设为 1,启用 TCP 时间窗扩展,可以提升客户端传输数据的效率,传输的数据可以在服务器端进行缓冲。把 net.ipv4.tcp_max_syn_backlog 设为比默认值 1024 更大的值,可以接受更多的并发连接。把 net.core.netdev_max_backlog 设为比默认值 1000 更大的值,有助于应对网络流量的爆发,特别是在使用千兆网络的情况下,允许更多的数据包排队等待内核处理。(熟悉Java网络编程的同学也知道,这些配置在Java应用中也可以配置) GC:优先选择G1;如果一台服务器有 64GB 内存,并且使用 5GB 堆内存来运行 Kafka,那么可以参考以下的配置:MaxGCPauseMillis 可以设为 20ms;InitiatingHeapOccupancyPercent 可以设为 35,这样可以让垃圾回收比默认的要早一些启动。 其他的生产建议:部署上建议跨机架(这是调度服务需要考虑的);同时建议在0.9版本之后把offset保存到broker来减少对zk的压力和依赖,不建议多集群复用zk服务。 基本的设计点 消费者的Rebalance 在0.9以前的client api中,consumer是要依赖Zookeeper的,来对整个集群的数据进行监听获取,并各自做rebalance的计算,会有惊群(herd effect)和脑力(brain split)的问题,同时会加重ZK本身的负载会有性能稳定性的问题。herd effect:任何Broker或者Consumer的增减都会触发所有的Consumer的Rebalance;split brain:每个Consumer分别单独通过Zookeeper判断哪些Broker和Consumer 宕机了,那么不同Consumer在同一时刻从Zookeeper“看”到的View就可能不一样,这是由Zookeeper的特性决定的,这就会造成不正确的Reblance尝试。在0.9中,不再用zookeeper,而是Kafka集群本身来进行consumer之间的同步,也就是下面说的机制。 类似broker中选了一个controller出来,消费也要从broker中选一个coordinator,用于分配partition。 看offset保存在那个partition 该partition leader所在的broker就是被选定的coordinator Rebalance的过程 consumer给coordinator发送JoinGroupRequest请求。 这时其他consumer发heartbeat请求过来时,coordinator会告诉他们,要reblance了。 其他consumer发送JoinGroupRequest请求。 所有记录在册的consumer都发了JoinGroupRequest请求之后,coordinator就会在这里consumer中随便选一个leader。然后回JoinGroupRespone,这会告诉consumer你是follower还是leader,对于leader,还会把follower的信息带给它,让它根据这些信息去分配partition consumer向coordinator发送SyncGroupRequest,其中leader的SyncGroupRequest会包含分配的情况。 coordinator回包,把分配的情况告诉consumer,包括leader。 注意以上partition的分配还是在Consumer端完成,这一设计的原因是根据业务的不同会有很多灵活的需求,从逻辑上看,这种灵活的分配需求来自消费端,而并非broker端,当消费端有新的需求时,如果需要调整的却是Broker端,显然有些怪异,也不够灵活。是包含了一种责任边界清晰化的设计考量。 当partition或者消费者的数量发生变化时,都得进行reblance。列举一下会reblance的情况: 增加partition 增加消费者 消费者主动关闭 消费者宕机了 coordinator自己也宕机了 消费者的offset状态维护 一个消费组消费partition,需要保存offset记录消费到哪,以前保存在zk中,由于zk的写性能不好,以前的解决方法都是consumer每隔一分钟上报一次。这里zk的性能严重影响了消费的速度,而且很容易出现重复消费。在0.10版本后,kafka把这个offset的保存,从zk总剥离,保存在一个名叫__consumeroffsets topic的topic中。写进消息的key由groupid、topic、partition组成,value是偏移量offset。topic配置的清理策略是compact。总是保留最新的key,其余删掉。一般情况下,每个key的offset都是缓存在内存中,查询的时候不用遍历partition,如果没有缓存,第一次就会遍历partition建立缓存,然后查询返回。确定consumer group位移信息写入__consumers_offsets 的哪个partition,具体计算是Consumer_Group与partition数量的哈希值。 生产者 分区器的实现,Schema管理 服务端文件组织 kafka的数据,实际上是以文件的形式存储在文件系统的。topic下有partition,partition下有segment,segment是实际的一个个文件,topic和partition都是抽象概念。在目录/${topicName}-{$partitionid}/下,存储着实际的log文件(即segment),还有对应的索引文件。每个segment文件大小相等,文件名以这个segment中最小的offset命名,文件扩展名是.log;segment对应的索引的文件名字一样,扩展名是.index。有两个index文件,一个是offset index用于按offset去查message,一个是time index用于按照时间去查,其实这里可以优化合到一起,下面只说offset index。总体的组织是这样的: 为了减少索引文件的大小,降低空间使用,方便直接加载进内存中,这里的索引使用稀疏矩阵,不会每一个message都记录下具体位置,而是每隔一定的字节数,再建立一条索引。 索引包含两部分,分别是baseOffset,还有position。 baseOffset:意思是这条索引对应segment文件中的第几条message。这样做方便使用数值压缩算法来节省空间。例如kafka使用的是varint。 position:在segment中的绝对位置。 查找offset对应的记录时,会先用二分法,找出对应的offset在哪个segment中,然后使用索引,在定位出offset在segment中的大概位置,再遍历查找message。 可靠性保证 可靠性的定义因人而异,比如Rocketmq的侧重在通过二次异步提交的方式来达到生产者的低且稳定的RT,虽然有加大丢失数据的风险,即CAP中的A(可用性);而Kafka是侧重在多副本复制而来的数据一致性,Kafka 的复制机制和分区的多副本架构是 Kafka 可靠性保证的核心,即CAP中的C(一致性)和ACID中的D(持久性)。可靠性这一系统行为是在一定条件下的权衡,需要针对具体的业务场景而定,这种权衡一般是指消息存储的可靠性和一致性的重要程度与可用性、高吞吐量、低延迟和硬件成本的重要程度之间的权衡。 Broker的可靠:每个partition分区可以有多个副本,其中一个副本是Leader。所有的事件都直接发送给Leader,或者直接从Leader读取事件。其他副本只需要与Leader保持同步,并及时复制最新的事件。当Leader不可用时,其中一个同步副本将成为新Leader。与 Zookeeper 的心跳6s内失败,或者不再获取新消息,或者获取消息滞后了 10s 以上,那么此副本就被认为是不同步副本。 replication.factor :topic级别的副本个数。如果复制系数为 N ,那么在 N -1 个 broker 失效的情况下,仍然能够从主题读取数据或向主题写入数据。所以,更高的复制系数会带来更高的可用性、可靠性和更少的故障。另一方面,复制系数 N 需要至少 N 个 broker,而且会有 N 个数据副本,也就是说它们会占用 N 倍的磁盘空间。比如金融级别的数据建议为5,日志监控类的数据为1。同时,可以通过broker.rack 参数来为每个 broker 配置所在机架的名字来提升高可用性。 unclean.leader.election: 在 broker 级别(实际上是在集群范围内)进行配置,它的默认值是 true,就是允许不同步的副本成为首领(也就是“不完全的选举”),那么我们将面临丢失消息的风险。如果把这个参数设为 false ,就要等待原先的首领重新上线,从而降低了可用性。在金融级的场景下,需要设置为false。 min.insync.replicas:主体级别的配置,最小同步的副本数。 Producer的可靠:即使我们尽可能把 broker 配置得很可靠,但如果没有对生产者进行可靠性方面的配置,整个系统仍然有可能出现突发性的数据丢失。 acks=n:0意味着oneway,最快但没有任何保证;1意味着Leader在收到消息并把它写入到分区数据文件(不一定同步到磁盘上)时会返回确认或错误响应;all 意味着首领在返回确认或错误响应之前,会等待所有同步副本都收到消息。 异常处理:分为可重试(比如网络抖动带来的网络异常)和不可重试(比如无法序列化,消息不符合规则等)。如果发生正常的首领选举,生产者会在选举时收到一个 LeaderNotAvailableException 异常,如果生产者能恰当地处理这个错误,只要重试就能保证消息发送OK(会带来潜在的消息重复问题,消息在append时做幂等控制还是比较困难)。 Consumer的可靠:消费者唯一要做的是跟踪哪些消息是已经读取过的,哪些是还没有读取过的。这是在读取消息时不丢失消息的关键。已提交消息是指已经被写入所有同步副本并且对消费者可见的消息——这里试想下如何在多副本间同步已提交的进度? 消费进度的提交:自动或显式。前者简单,但可能会导致没消费成功而推进offset,或者重复消费。显式提交需要考虑频度(是每次消费完还是异步延迟),是性能和重复消息数量之间的权衡。 监控可靠性: 对于生产者来说,最重要的两个可靠性指标是消息的 error-rate 和 retry-rate(聚合过的)。如果这两个指标上升,说明系统出现了问题。 对于消费者来说,最重要的指标是 consumer-lag,该指标表明了消费者的处理速度与最近提交到分区里的偏移量之间还有多少差距。理想情况下,该指标总是为 0,消费者总能读到最新的消息。—— 在最Kafka2.0版本中,有另一个lag指标:当分区 renteion 时间很短而导致消费者跌出可消费范围时(out-of-range),consumer-lag指标不能完全针对潜在的危险为用户报警。因此加入了另一个“领先”指标(lead metrics),定义为分区首端(log-start-offset)与消费者在分区上的位置距离,当此指标趋近于零时,代表消费者有跌出可消费范围因而丢失数据的危险。 监控数据流及时地读取:为了确保数据能够被及时读取,你需要知道数据是什么时候生成的。这方面Notify,RocketMQ定义的就比较好,有borntime(客户端生成时间),gmtCreate(到达服务端时间)等;同时加上有eagleeye tracing这样的链路分析会更加的精细。PS,这里面需要提一下,Kafka的“自产自销”的好用处:增加一个“监控消费者”的Topic,这个消费者订阅一个特别的主题,它只进行消息的计数操作,并把数值与生成的消息数量进行对比,这样我们就可以在没有消费者的情况下仍然能够准确地监控生产者。
继续上一篇 (空学Kafka之一)[https://www.atatech.org/articles/145913] 构建数据通道 考量点 及时性,可靠性,吞吐量,安全性(通道安全,审计等),数据格式的上线兼容,ETL or ELT,统一还是专属(比如GoldenGate是oracle私有的,有很强的耦合性),优先选择Kafka Connect 深入浅出Connect 连接器插件实现了 Connector API,API 包含了两部分内容。大致上是分而治之的思想,连接器相当于分拆器splittor,任务相当于拆分后的具体执行器executer。 连接器:负责以下三件事。 决定需要运行多少个任务。 按照任务来拆分数据复制。 从 worker 进程获取任务配置并将其传递下去。 任务:负责将数据移入或移出 Kafka。 相比较直接采用Kafka的publisher/consumer API来实现数据流入和流出逻辑,Connect做了很多基础性的工作,一方面体现在上面的Connector API的抽象定义和生态中丰富的Connector实现,另一方面体现在worker 进程,负责 REST API、配置管理、可靠性、高可用性、伸缩性和负载均衡,有经验的开发人员都知道,编写代码从 Kafka 读取数据并将其插入数据库只需要一到两天的时间,但是如果要处理好配置、异常、REST API、监控、部署、伸缩、失效等问题,可能需要几个月。如果你使用连接器来实现数据复制,连接器插件会为你处理掉一大堆复杂的问题。同时,提供的偏移量(不单单是Kafka的消费进度,也包含源数据源的进度)跟踪机制简化了连接器的开发工作,并在使用多个连接器时保证了一定程度的行为一致性。 源连接器返回的记录里包含了源系统的分区和偏移量,worker 进程将这些记录发送给 Kafka。如果 Kafka 确认记录保存成功,worker 进程就把偏移量保存下来。偏移量的存储机制是可插拔的,一般会使用 Kafka 主题来保存(又是自己吃自己的狗粮,不过,用一个topic保存进度是不是有点奢侈?)。如果连接器发生崩溃并重启,它可以从最近的偏移量继续处理数据。 跨集群数据镜像 想到了阿里由HSF构造的分布式集群其实是一个大集群,即便现在机房分布在深圳,张北的情况下仍然是统一大集群,也就是没有做到本机房内集群的收敛——这也多亏了Configserver等基础服务可以支撑百万集群,本机房调用是非默认的行为。从服务治理角度看,本人觉得同城内为一集群更合理,跨机房的服务集成调用需要特殊的治理管控。 跨集群镜像的应用场景 区域集群和中心集群:跨DC的数据同步 冗余Disaster Recovery:集群灾备 云迁移:混合云的情形,自有数据中心与云服务的集群同步 多集群架构 现实限制包含高延迟(技术上最大的问题),宽带有限,公网宽带费用带来的高成本,考虑以上限制因素,需要给定以下架构原则。 每个数据中心至少需要一个集群。 每两个数据中心之间的数据复制要做到每个事件仅复制一次(除非出现错误需要重试)。 如果有可能,尽量从远程数据中心读取数据,而不是向远程数据中心写入数据。 Hub和Spoke架构 这种架构适用于一个中心 Kafka 集群对应多个本地 Kafka 集群的情况,如下图所示。这种架构的好处在于,数据只会在本地的数据中心生成,而且每个数据中心的数据只会被镜像到中央数据中心一次。只处理单个数据中心数据的应用程序可以被部署在本地数据中心里,而需要处理多个数据中心数据的应用程序则需要被部署在中央数据中心里。因为数据复制是单向的,而且消费者总是从同一个集群读取数据,所以这种架构易于部署、配置和监控。不足是一个数据中心的应用程序无法访问另一个数据中心的数据。 双活(active-active)架构 这种架构的主要好处在于,它可以为就近的用户提供服务,具有性能上的优势,而且不会因为数据的可用性问题(在 Hub 和 Spoke 架构中就有这种问题)在功能方面作出牺牲。第二个好处是冗余和弹性。因为每个数据中心具备完整的功能,一旦一个数据中心发生失效,就可以把用户重定向到另一个数据中心。这种重定向完全是网络的重定向,因此是一种最简单、最透明的失效备援方案。这种架构的主要问题在于,如何在进行多个位置的数据异步读取和异步更新时避免冲突。比如镜像技术方面的问题——如何确保同一个数据不会被无止境地来回镜像?而数据一致性方面的问题则更为关键。 如果能够很好地处理在从多个位置异步读取数据和异步更新数据时发生的冲突问题,那么我们强烈建议使用这种架构。这种架构是我们所知道的最具伸缩性、弹性、灵活性和成本优势的解决方案。所以,它值得我们投入精力去寻找一些办法,用于避免循环复制、把相同用户的请求粘在同一个数据中心,以及在发生冲突时解决冲突。双活镜像(特别是当数据中心的数量超过两个)的挑战之处在于,每两个数据中心之间都需要进行镜像,而且是双向的。如果有 5 个数据中心,那么就需要维护至少 20 个镜像进程,还有可能达到 40 个,因为为了高可用,每个进程都需要冗余。 主备(active-standby)架构 简单容易实施,但浪费一个集群。 MirrorMaker MirrorMaker完全是无状态的,收后再转发有点代理的意思:性能在于线程模型的设计。尽量让 MirrorMaker 运行在目标数据中心里。也就是说,如果要将 NYC 的数据发送到 SF,MirrorMaker 应该运行在 SF 的数据中心里。因为长距离的外部网络比数据中心的内部网络更加不可靠,如果发生了网络分区,数据中心之间断开了连接,那么一个无法连接到集群的消费者要比一个无法连接到集群的生产者要安全得多。如果消费者无法连接到集群,最多也就是无法读取数据,数据仍然会在 Kafka 集群里保留很长的一段时间,不会有丢失的风险。相反,在发生网络分区时,如果 MirrorMaker 已经读取了数据,但无法将数据生成到目标集群上,就会造成数据丢失。所以说,远程读取比远程写入更加安全。**如果跨数据中心流量需要加密,那么最好把 MirrorMaker 放在源数据中心,让它读取本地的非加密数据,然后通过 SSL 连接将数据生成到远程的数据中心。这个时候,使用 SSL 连接的是生产者,所以性能问题就不那么明显了。在使用这种方式时,需要确保 MirrorMaker 在收到目标 broker 副本的有效确认之前不要提交偏移量,并在重试次数超出限制或者生产者缓冲区溢出的情况下立即停止镜像。 流式处理 何谓流数据(streaming Data 或 数据流) 无边界:首先,数据流是无边界数据/事件集的抽象表示。无边界意味着无限和持续增长。 有序的:事件的发生总是有个先后顺序。 不可变:事件一旦发生,就不能改变——时光不可倒流,每个事件都相当于一个DB事务(ACID) 可重播:这是事件流非常有价值的一个属性。用户可以很容易地找出那些不可重播的流(流经套接字的 TCP 数据包就是不可重播的),但对于大多数业务来说,重播发生在几个月前(甚至几年前)的原始事件流是一个很重要的需求。可能是为了尝试使用新的分析方法纠正过去的错误,或是为了进行审计。这也就是为什么我们相信 Kafka 能够让现代业务领域的流式处理大获成功——可以借助 Kafka 来捕捉和重播事件流。如果没有这项能力,流式处理充其量只是数据科学实验室里的一个玩具而已。 这里额外提一下理念:prefer Event(已发生的确定的事实) over Command 流式处理的基本点 时间:最核心最基本的概念(想起以前做蚂蚁财富的基金系统的时间也是无处不在,各种类型的时间在眼前飘。。。),包含多种类型:事件发生时间,日志追加时间,处理时间,同时注意时区问题。 状态:事件与事件之间的信息被称为“状态”。这些状态一般被保存在应用程序的本地变量里。例如,使用本地散列表来保存移动计数器。不过,这不是一种可靠的方法,因为如果应用程序关闭,状态就会丢失,结果就会发生变化,而这并不是用户希望看到的。所以,要小心地持久化最近的状态,如果应用程序重启,要将其恢复。 流与表的二元性:其实可以看看event sourcing和CQRS的一些架构思想和具体实践来理解这块;表-->>流,需要捕捉到在表上所发生的变更,将“insert”、“update”和“delete”事件保存到流里,即CDC(Change Data Capture);流-->>表,需要“应用”流里所包含的所有变更,这也叫作流的“物化”。首先在内存里、内部状态存储或外部数据库里创建一个表,然后从头到尾遍历流里的所有事件,逐个地改变状态。在完成这个过程之后,得到了一个表,它代表了某个时间点的状态,即materialized veiw。 时间窗口:窗口大小、变化窗口、可更新的时间长度等(想起来HSF的TPS限流规则实现,TPS限流也算是时间窗口敏感的类似场景,我是觉得联想到以前类似的场景就会更加容易理解透新的场景);滚动窗口与跳跃窗口(HSF tps限流规则是滚动来刷新token数) 流处理的设计模式 单事件处理:无状态,单纯计算逻辑,比如filter,map,execute等。(Event-Driven Microservice这种基于pub-sub的消息驱动其实就是最简单最常见的流处理形态;以前认为ESB这种模式的问题在于依赖一个消息中心,具有单点问题,所以相比HSF这类的SOA架构要落后,现在再看以前的认识是片面的;在延展一下想到阿里现在的中台,曾经参加过一关于星环的测试,真是复杂,高大上的无厘头的高度抽象,如果有了数据总线/数据中台,很多时候可能自建系统会更加高效且稳定,比当前的中台还要中台——中台不就是为了效率么?难道是为了利益和底盘?比如蚂蚁的paycore/tradecore就很容易调用他们的API来集成,而二级域都有自己对应的下单流水系统来控制各业务的状态流转。新加:在了解了kafka的事务性语义后,Kafka的事务和RocketMQ/Notify的事务消息还真不是一回事,差别很大,前者是Stream处理中的consumer-transform-produce或者跨分区发送消息时的原子性,后者是以业务为确定事件后消息可靠性,所以相比较而言,RocketMQ更适合交易系统类的消息集成场景,如果非要选择Kafka作为交易场景的事务性消息,就需要先落业务DB,完成业务的事务性提交,Kafka通过Connect+Kafka的事务性语义消费DB binlog事务来驱动下游系统。) 使用本地状态:每一个操作都是基于组的聚合操作,例如,基于各个股票代码进行聚合,而不是基于整个股票市场。我们使用了一个 Kafka 分区器来确保具有相同股票代码的事件总是被写入相同的分区。应用程序的每个实例从分配给它们的分区上获取事件(这是 Kafka 的消费者保证)。也就是说,应用程序的每一个实例都可以维护一个股票代码子集的状态。如下图所示。需要解决下列的一些问题。 内存使用:应用实例必须有可用的内存来保存本地状态。 持久化:要确保在应用程序关闭时不会丢失状态,并且在应用程序重启后或者切换到另一个应用实例时可以恢复状态。Streams 可以很好地处理这些问题,它使用内嵌的 RocksDB 将本地状态保存在内存里,同时持久化到磁盘上,以便在重启后可以恢复。本地状态的变更也会被发送到 Kafka 主题上。如果 Streams 节点崩溃,本地状态并不会丢失,可以通过重新读取 Kafka 主题上的事件来重建本地状态。例如,如果本地状态包含“IBM 当前最小价格是 167.19”,并且已经保存到了 Kafka 上,那么稍后就可以通过读取这些数据来重建本地缓存。这些 Kafka 主题使用了压缩日志,以确保它们不会无限量地增长,方便重建状态。 再均衡:有时候,分区会被重新分配给不同的消费者。在这种情况下,失去分区的实例必须把最后的状态保存起来,同时获得分区的实例必须知道如何恢复到正确的状态。 多阶段处理和重分区:这种多阶段处理对于写过 Map-Reduce 代码的人来说应该很熟悉,因为他们经常要使用多个 reduce 步骤。如果写过 Map-Reduce 代码,就应该知道,处理每个 reduce 步骤的应用需要被隔离开来。与 Map-Reduce 不同的是,大多数流式处理框架可以将多个步骤放在同一个应用里,框架会负责调配每一步需要运行哪一个应用实例(或 worker)。 ——( 想起来了以前做基金文件的处理模式:Split-》Execute-》merge-》finalize,这一模式相比就是多了聚合。) 流与表的结合:这种方式最大的问题在于,外部查找会带来严重的延迟,一般在 5~15ms 之间。这在很多情况下是不可行的。另外,外部数据存储也无法接受这种额外的负载——流式处理系统每秒钟可以处理 10~50 万个事件,而数据库正常情况下每秒钟只能处理 1 万个事件,所以需要伸缩性更强的解决方案。为了获得更好的性能和更强的伸缩性,需要将数据库的信息缓存到流式处理应用程序里。不过,要管理好这个缓存也是一个挑战。比如,如何保证缓存里的数据是最新的?如果刷新太频繁,那么仍然会对数据库造成压力,缓存也就失去了作用。如果刷新不及时,那么流式处理中所用的数据就会过时。所以有了通过Connect的CDC来刷新缓存,如下图。 基于时间窗口的流与流的连接:需要考虑时间窗口 重新处理:需要考虑是否需要重置状态或者新开应用分组 Kafka Stream 优势:类库形式与业务应用部署在一起,相比Spark,flink,Storm等无需额外的依赖按照和额外的调度服务。想到了蚂蚁的三层调度框架(小而美,解决了大部分的批处理问题),让复杂的事情简单了。 伸缩性和面向故障容错能力是基于Kafka的分区特性,Consumer的自动负载均衡特性,这些使得Kafka天然具备数据流平台的先天基础。 Kafka Stream API用起来感觉就像JDK8中的Stream接口一样
来源 在对Dubbo新版本做性能压测时,无意中发现对用例中某个TO(Transfer Object)类的一属性字段稍作修改,由Date变成LocalDateTime,结果是吞吐量由近5w变成了2w,RT由9ms升指90ms。 在线的系统,拼的从来不仅仅是吞吐量,而是在保证一定的RT基础上,再去做其他文章的, 也就是说应用的RT是我们服务能力的基石所在, 拿压测来说, 我们能出的qps/tps容量, 必须是应用能接受的RT下的容量,而不是纯理论的数据,在集团云化的过程中计算过,底层服务的RT每增加0.1ms,在应用层就会被放大, 整体的成本就会上升10%以上。 要走向异地,首先要面对的阿喀琉斯之踵:延时,长距离来说每一百公里延时差不多在1ms左右,杭州和上海来回的延迟就在5ms以上,上海到深圳的延迟无疑会更大,延时带来的直接影响也是响应RT变大,用户体验下降,成本直线上升。 如果一个请求在不同单元对同一行记录进行修改, 即使假定我们能做到一致性和完整性, 那么为此付出的代价也是非常高的,想象一下如果一次请求需要访问10 次以上的异地 HSF 服务或 10 次以上的异地 DB调用, 服务再被服务调用,延时就形成雪球,越滚越大了。 普遍性 关于时间的处理应该是无处不在,可以说离开了时间属性,99.99%的业务应用都无法支持其意义,特别是像监控类的系统中更是面向时间做针对性的定制处理。 在JDK8以前,基本是通过java.util.Date来描述日期和时刻,java.util.Calendar来做时间相关的计算处理。JDK8引入了更加方便的时间类,包括Instant,LocalDateTime、OffsetDateTime、ZonedDateTime等等,总的说来,时间处理因为这些类的引入而更加直接方便。 Instant存的是UTC的时间戳,提供面向机器时间视图,适合用于数据库存储、业务逻辑、数据交换、序列化。LocalDateTime、OffsetDateTime、ZonedDateTime等类结合了时区或时令信息,提供了面向人类的时间视图,用于向用户输入输出,同一个时间面向不同用户时,其值是不同的。比如说订单的支付、发货时间买卖双方都用本地时区显示。可以把这3个类看作是一个面向外部的工具类,而不是应用程序内部的工作部分。 简单说来,Instant适用于后端服务和数据库存储,而LocalDateTime等等适用于前台门面系统和前端展示,二者可以自由转换。这方面,国际化业务的同学有相当多的体感和经验。 在HSF/Dubbo的服务集成中,无论是Date属性还是Instant属性肯定是普遍的一种场景。 问题复现 Instant等类的性能优势 以常见的格式化场景举例 @Benchmark @BenchmarkMode(Mode.Throughput) public String date_format() { Date date = new Date(); return new SimpleDateFormat("yyyyMMddhhmmss").format(date); } @Benchmark @BenchmarkMode(Mode.Throughput) public String instant_format() { return Instant.now().atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern( "yyyyMMddhhmmss")); } 在本地通过4个线程来并发运行30秒做压测,结果如下。 Benchmark Mode Cnt Score Error Units DateBenchmark.date_format thrpt 4101298.589 ops/s DateBenchmark.instant_format thrpt 6816922.578 ops/s 可见,Instant在format时性能方面是有优势的,事实上在其他操作方面(包括日期时间相加减等)都是有性能优势,大家可以自行搜索或写代码测试来求解。 Instant等类在序列化时的陷阱 针对Java自带,Hessian(淘宝优化版本)两种序列化方案,压测序列化和反序列化的处理性能。 Hessian是集团内应用的HSF2.2和开源的Dubbo中默认的序列化方案。 @Benchmark @BenchmarkMode(Mode.Throughput) public Date date_Hessian() throws Exception { Date date = new Date(); byte[] bytes = dateSerializer.serialize(date); return dateSerializer.deserialize(bytes); } @Benchmark @BenchmarkMode(Mode.Throughput) public Instant instant_Hessian() throws Exception { Instant instant = Instant.now(); byte[] bytes = instantSerializer.serialize(instant); return instantSerializer.deserialize(bytes); } @Benchmark @BenchmarkMode(Mode.Throughput) public LocalDateTime localDate_Hessian() throws Exception { LocalDateTime date = LocalDateTime.now(); byte[] bytes = localDateTimeSerializer.serialize(date); return localDateTimeSerializer.deserialize(bytes); } 结果如下。可以看出,在Hessian方案下,无论还是Instant还是LocalDateTime,吞吐量相比较Date,都出现“大跌眼镜”的下滑,相差100多倍;通过通过分析,每一次把Date序列化为字节流是6个字节,而LocalDateTime则是256个字节,这个放到网络带宽中的传输代价也是会被放大。 在Java内置的序列化方案下,有稍微下滑,但没有本质区别。 Benchmark Mode Cnt Score Error Units DateBenchmark.date_Hessian thrpt 2084363.861 ops/s DateBenchmark.localDate_Hessian thrpt 17827.662 ops/s DateBenchmark.instant_Hessian thrpt 22492.539 ops/s DateBenchmark.instant_Java thrpt 1484884.452 ops/s DateBenchmark.date_Java thrpt 1500580.192 ops/s DateBenchmark.localDate_Java thrpt 1389041.578 ops/s 分析解释 Hession中其实是有针对Date类做特殊处理,遇到Date属性,都是直接获取long类型的相对来做处理。 通过分析Hessian对Instant类的处理,无论是序列化还是反序列化,都需要Class.forName这个耗时的过程。。。,怪不得throughput急剧下降。 延展思考 1) 可以通过扩展实现Instant等类的com.alibaba.com.caucho.hessian.io.Serializer,并注册到SerializerFactory,来升级优化Hessian。但会有前后兼容性上,这个是大问题,在集团内这种上下游依赖比较复杂的场景下,极高的风险也会让此不可行。从这个角度看,只有建议大家都用Date来做个TO类的首选的时间属性。 2) HSF的RPC协议从严格意义上讲是 Session握手层的协议定义,其中的版本识别也是这个层面的行为,而业务数据的presentation展示层是通过Hessian等自描述的序列化框架来实现,这一层其实是缺少版本识别,从而导致升级起来就异常困难。
环境配置 Client与服务端分处两台ECS,配置为8C8G OS:alios7.x86_64 JDK:ajdk-8_6_11-b380 启动参数:-Xms1g -Xmx1g 压测配置:同步调用(最常见的调用方式),200个客户端并发线程,预热30s,压测时长600s (10分钟) 若干解释 本次压测是单对单的同步调用,也就是一台client发送请求到server,建立的网络连接是一个,按照Netty的处理策略是一个Eventloop线程在工作处理io事件,这样的测试条件可能会导致io线程成为瓶颈。减轻io线程的工作负荷(比如把序列化和反序列化等逻辑从io线程中腾挪到biz线程阶段)在此背景下是一个优化点,但是考虑到真实使用场景下,会是多个连接,io层(也就是网络层)一般不会是瓶颈,所以减轻io的技巧不具备太多实用价值。 服务端的线程策略是默认的dubbo策略,即io线程转给biz线程的两阶段,这一策略在服务端是纯cpu逻辑的性能测试场景下会弱化,但考虑到其普适性,我们还是坚持以此策略为测试。相比较其他RPC库可能是不一样的,也是影响性能的重要因素。 网络上有一些rpc库/框架的性能比较文章或者代码库,但是基本是单纯的简单调用,也就是侧重在网络性能的分析比较。但是像Dubbo/HSF更多的考虑是软负载,服务治理等功能特性的支持,甚至扩展机制。在简单测试中肯定会吃亏。从这个角度看,单纯一味追求性能的RPC框架未必是好的。 最后也是重要一点,rpc测试中必须考虑RT,一方面RT对于业务应用的意义相比较TPS显得一样重要,RT延迟会受到多方面因素(比如GC,网络质量等)的影响,同时,RT的质量(也就是RT的正太分布图,比如99%的RT是8ms以内,而最长的rt可能是50ms,这个在很多场景下也是无法被接受)也是比较重要的考量; 另一方面,在压测过程中始终达不到系统资源(一般是网络和CPU)的极限,这时就会出现优化改进后tps基本没提升,但RT成为衡量因素。 测试中用到了JMH,JFR/JMC,这两个东东还不错,尤其是JMH不仅方便压测,也有自带一些profiler。http://psy-lob-saw.blogspot.com/2016/02/why-most-sampling-java-profilers-are.html 本文中多个优化点离不开@陆龟 的帮助和支持,尤其是从优化点8开始均有@陆龟提供。当然,现在的Dubbo优化也只是刚开始。 优化点及数据 数据解释 以下面出现的第一张图为例。 第二行Client.createUser: 在createUser这个用例下,吞吐量(thrpt)是每毫秒42.819,也就是qps是42819。 第四行Client.createUser:在createUser这个用例下,平均RT是4.852ms,误差在0.008ms。 第九行Client**·createUser·p0.99:在createUser这个用例下,99%请求的RT都是在12.665ms以内 第十二行Client**·createUser·p1.00:在createUser这个用例下,100%请求的RT都是在73.138ms以内,也就说最大RT的那一次调用是73.138。 未优化的情形 Dubbo 2.7.1-release Dubbo 2.6.5 release Dubbo 3.0.0-SNAPSHOT 优化1:3.0版本Load高吞吐量低的问题 3.0的当前版本是相当让人惊讶的结果,没办法重复跑了几遍,都是如此。服务端OK,客户端的load非常高。排查过程:考虑到load极高而CPU,而事实上CPU(无论是sys,还是us)没有沾满100%,先从thread分析。从stackprofiler来分析,问题很明显:在AsyncToSynInvoker中调用了CompletableFuture(简称CF)的get方法。打开CF的相关代码,会发现先通过自旋2的8次方再进入阻塞队列进行等待。在RPC的场景下,网络等待一般都是ms级别的,这样自旋存在着浪费CPU的问题,更适合的方式是通过信号锁的方式来处理。 解决方法: 其实Google了很多CF的文章,想找到一种比较好的使用方式,也参照了些代码实现,最后还是认为是以前HSF中基于Guava的Future实现更合适。 优化效果:性能由6300tps提升到38458tps,提升了510%,具体见截图数据。 优化2:FGC问题 以上的CF使用问题虽然解决了一大问题,但是性能还是不及2.x版本,尤其是在rt方面,从上面的数据截图可以看出,在2.7.1版本上p99的rt在12.66ms,而3.0版本上的p99达到了44ms,这差距实在是无语。。。。 特别感谢@刘军在优化1时也在本地跑了一下,本地也跑出来OutofMemory。。。 本人在性能环境做了GC方面的压测,在接近1小时的压测过程内,没有遇到内存溢出,但是基本上每三秒就会一次FGC,这应该是导致RT增加了2倍多的原因 由于无outmemeryerror,同时加上FGC是由于old区的满引起,所以猜测是对象的生命周期太长,导致无法在NEW区中快速回收。通过几次查看HEAP的对象实例,发下排名前几位且与Dubbo相关的对象是DefaultFuture 问题根源:在timeout的task中持有了DefaultFuture这个比较重的对象,而timeoutTask是和超时时间一样生命市场的对象。通过优化TimeoutTask对象,解决此问题。 优化效果:fgc解除,rt提升明显。见下图数据:old区基本不增长了,FGC也不再随着时间推移而增长。最关键的是rt由之前的44ms降到了13ms,基本接近2.7.1版本。 以上两个问题其实更多是一种BUG存在的问题排查,经过优化1和优化2后,当前最新版本从qps和rt两方面基本上达到了Dubbo的正常性能水平。顺便提一下,这也再次印证本人之前说的:没有rt的qps性能测试都是扯淡!! 优化3:返回类型的改进 根据jmh的stackprofiler结果看,每次结果返回都需要通过java reflection api获取returnType,这里的计算比较多。见截图。 优化后的结果见下图,性能从39797提升到41784,有5%的提升。 优化4:判断invokerMode的优化 0.0% 6.1% sun.reflect.Reflection.getCallerClass java.lang.Class.getMethod org.apache.dubbo.rpc.support.RpcUtils.getReturnType org.apache.dubbo.rpc.support.RpcUtils.isReturnTypeFuture org.apache.dubbo.rpc.support.RpcUtils.getInvokeMode org.apache.dubbo.rpc.protocol.AbstractInvoker.invoke org.apache.dubbo.rpc.protocol.AsyncToSyncInvoker.invoke org.apache.dubbo.monitor.support.MonitorFilter.invoke org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke org.apache.dubbo.rpc.protocol.dubbo.filter.FutureFilter.invoke org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke org.apache.dubbo.rpc.filter.ConsumerContextFilter.invoke org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke org.apache.dubbo.rpc.listener.ListenerInvokerWrapper.invoke org.apache.dubbo.rpc.proxy.InvokerInvocationHandler.invoke org.apache.dubbo.common.bytecode.proxy3.getUser 优化后从41784提升到43659,提升在5%,同时RT也提升到13ms内,具体见下图数据。 优化5:消费端使用发起同步调用时去除额外的共享线程池 当前Dubbo的实现在同步调用方式下,对于response的处理是单独创建了一个线程池做处理,即 “I/O -> 共享线程池 -> 业务线程” 的切换,这样就额外增加了一次Context Switch。经过@刘军(陆龟)的优化改进,做一下实现。 同步:“I/O -> 业务线程“ 异步:保持不变,“I/O -> 共享线程池” 看完结果,跟预期的tps与rt都提升相比较,还是有些疑惑:基本上没有提升的迹象。分析下来,有两个原因可以稍作解释:一是这一块的改动比较复杂,需要关注比较细致的并发技巧,现在的改进还不足,需要进一步完善;二是当前阶段的瓶颈不在于此,也就是说Context Switch的频繁对于当前测试的影响微不足道。 优化6:消费端实现rpc模块和Remoting模块的Future桥接 问题发现:通过jstack发现,io线程有比较多的比例是耗费在CF的complete过程,如下截图。 解决方案:remoting层复用rpc层的CompletableFuture来解决。性能提高了10%以上。 优化7:char[]的分配导致gc 从JProfiler allocation hot spot 也反映出URL相关操作分配占比较高 注:除GC意外,可能由于分配导致的耗时操作 优化8:AsyncRpcResult.thenApplyWithContext重复执行 AsyncRpcResult.thenApplyWithContext在每个filter重复执行,占用较大比例的CPU时间可能和频繁操作RpcContext.get/setContext有关(consumer单线程场景比例较高,可能和provider端threadlocal初始化有关) 优化9:YGC频率较高 请求过程中避免一些重复的对象创建,除了5中提到URL操作导致String分配外,考虑Request、Response的复用与加速回收 如利用Netty的Recycler机制等。 优化10:序列化从I/O线程切到业务线程池 decode.in.io=true 从provider端I/O thread状态来看,在32并发的情况下,decode在I/O对provider端还是有较大的runnable集中期的,可以考虑调整试一试 消费端可以尝试调整I/O线程数量,或者减少I/O线程中可能的阻塞/耗时操作 目前发现的存在于I/O线程中慢操作有: Future.complete()状态转换 park -> unpark ConcurrentHashMap.put("LAST_READ_TIME", long);造成的锁竞争,参见 优化11 业务线程send调用channel.isConnect(),与I/O线程read等造成竞争,打破了Netty关于channel绑定单线程的模型,见下图 优化11:NetUtils.getLocalHost() 比如ConsumerContextFilter中有调用 public static String getLocalHost() { InetAddress address = getLocalAddress(); return address == null ? Constants.LOCALHOST_VALUE : address.getHostAddress(); } 优化12: 避免每次RpcContext.getContext(),注意保存引用 这个问题经充分预热后可能会好很多。这点从theyApplyWithContext中也能反映出来 优化13: 业务线程、I/O线程同时更新Channel.ConcurrentHashMap导致的锁竞争 此点尚存义attributes.put(LAST_READ_TIME, long),没必要做到精确,可使用普通HashMap心跳避免更新HashMap?
背景 应该是去年在摸索servicemesh时,看过些关于Kubernetes资料(主要是ppt载体的信息),也部署并体验了一番Openshift,同时留下过两篇关于servicemesh和Kubernetes的两篇文章。 Dubbo/HSF在Service Mesh下的思考和方案 Dubbo与kubernetes的集成思考 但个人感觉以前的了解是有点片面和零散(其实网上大多分享都是如此哈)。这次组里需要评估分析下Dubbo在云原生的思路,所以这几天我就花了些时间温习了这块的知识,看了《Docker实践》《Kubernetes实战》两本书,希望能够全面而分层次地做一些记录。 为了说清楚云原生,先从三个问题开始,希望大家能自己思考下。 三个问题 docker是为了解决什么问题出现的?提供了哪些核心能力? Kubernetes是为了解决什么问题出现的? 在容器化基础上增加了哪些核心能力? ServiceMesh是为了解决什么问题出现的? 在k8s基础上增加了哪些核心能力? Docker记录 这里主要是阅读《Docker实践》的记录。 Docker是一种Linux容器工具集,它是为构建(Build)、交付(Ship)和运行(Run)分布式应用而设计的。容器化保证应用的独立可重复性;虚拟化来保证物理主机安全的功能模式。 Docker的本质 Docker首先是一种契约,是串联了整个应用生命周期的契约,核心价值是加快软件交付的效率,提高生产力。 Docker与虚拟机的定位是本质不一样的,Docker并不是一项虚拟机技术(这其实是容器技术与虚拟机技术的区别)。它不会模拟一台机器的硬件。Docker定位于应用侧,是对应用(包括其配置,环境依赖)的封装——通过标准化的dockerfile规范,而虚拟机定位于OS层,偏重于资源管理。 Docker也可以与Chef,Jenkins做深度整合。 Docker的Hub是其应用程序的管理中心,最能够体现其应用侧的特性,每个镜像都可以命名,tag,唯一ID。 与软件各环节的结合 Docker与开发: 开发环境的一致,进程即环境;传统配置工具(make,chef,puppet等)与Docker协作;构建更小的镜像;Docker对于开发的友好性,最好隐藏dockerfile,使得source到Image自动化。 Docker与Devops:从Git到Docker Hub到Jecksin的自动化工作流;从持续提交(source)-》持续集成(test) -》持续部署(蓝布发布,灰度发布等)-》持续交付(版本管理,基础环境的整体搭建,资源按需伸缩); Docker与生产环境:其实就是swarm,Kubernetes等PaaS层的东东的用武之地。 多宿主机情况下容器的运行时状态和配置管理 多宿主机情况下容器间的“串联编排”,安全,日志,问题诊断排查等服务管理 Kubernetes记录 前面说了有了Docker化后的应用后,需要一平台级的东东来管理这些Docker容器。以下是阅读《Kubernetes实战》的一些点。 为什么K8S Kubernetes的流行跟Docker是息息相关:Docker的流行激活了一直不温不火的PaaS。Kubernetes可以说是Google借助着容器领域的爆发,对于其巨大规模数据中心管理的丰富经验的一次实践,旨在建立新的技术业界标准。Google从2004年起就已经开始使用容器技术了,于2006年发布了Cgroup,而且内部开发了强大的集群资源管理平台Borg和Omega,这些都已经广泛使用在Google的各个基础设施中,而Kubernetes的灵感来源于Google的内部Borg系统,更是吸收了包括Omega在内的容器管理器的经验和教训。 抛个问题吧。为什么Kubernetes在最早的compose,swarm,甚至mesos中能够脱颖而出? 这个跟它的模型抽象(也对应了核心概念)是有很大关系。 核心概念 Pod:是若干相关容器的组合,Pod包含的容器运行在同一台宿主机上,这些容器使用相同的网络命名空间、IP地址和端口,相互之间能通过localhost来发现和通信。另外,这些容器还可共享一块存储卷空间。在Kubernetes中创建、调度和管理的最小单位是Pod,而不是容器,Pod通过提供更高层次的抽象,提供了更加灵活的部署和管理模式。 Controller: 控制器,是任务执行的逻辑载体,有Controller Manager负载调度执行。ReplicationController是其中之一的具体实现,是弹性伸缩、滚动升级的实现核心。基本上每种实体model都会有对应的controller,比如Node,Service,Endpoint,SA,PV,Deployment,Job等等,也可以扩展实现自己的controller。 Service:是真实应用服务的抽象,定义了Pod的逻辑集合和访问这个Pod集合的策略。Service将代理Pod对外表现为一个单一访问接口,外部不需要了解后端Pod如何运行,这给扩展和维护带来很多好处,提供了一套简化的服务代理和发现机制。另外,Service不仅可以代理Pod,还可以代理任意其他后端,比如运行在Kubernetes外部的服务——这时候不需要selector,但需要手动定义对应的与Service同名的Endpoint。 Label:键值对,可以对POD做标准,实现service,RC对于POD的松耦合。 Node: 宿主机? 架构设计 list-watch事件的控制器模式 CI/CD主流选型对比 持续集成与持续部署(Continuous Integration & Deployment)应该是最能体现PaaS平的特色之处,各家云厂商都在基于Kubernetes提供PaaS服务,在CI/CD上的产品特性和用户使用体验是区分这一服务的重要之处。 下图是从别处Copy来的简单对比。个人理解,广义上的CI/CD应该包含从创建项目开始,囊括了需求管理,设计管理,到代码管理,到环境配置,到代码(镜像)构建,最后到日常/测试/预发/灰度/线上等环境的部署,这可能也算是核心切入点之一吧,也就是发挥集成优势,打通相关环节,提供一站式应用服务。 可以花点时间了解以下主流的CI/CD,取长补短: Jenkins:Hudson?️,采用Java实现,用的最广泛。 Teamcity:10年前曾经用过一点点,留下了功能更多更强的印象。 CircleCI Travis CI:Apache的项目都是基于TC来做集成测试,每次PR都有TC任务,做单元测试,同时做各种代码规范的检查。 Drone: API对象与元数据 无论是pod,Service,还是Node,Endpoint,PV等等都是Kubernetes的基本对象,有三种方式来访问和操作这些对象。 命令行工具kuberctl:一般演示中都是基于此。 直接访问Rest API:Kubernetes API Server作为Kubernetes系统的入口,以REST API接口方式提供给外部调用,同时集成了Swagger来做API的定义描述。 SDK:比如Java中fabric8提供了Kubernetes-client。 API对象的元数据用来定义API对象的基本信息,体现在定义中的metadata字段,包含以下属性。 •namespace:指定API对象所在的Namespace。 •name:指定API对象的名称。 •labels:设置API对象的Label。 •annotations:设置API对象的Annotation。 Namespace Namespace是Kubernetes提供的多租户,不同的项目、团队或者用户可以通过Namespace进行区分管理,并且设置安全控制和其他策略。绝大部分API对象(除了Node)归属于Namespace,API对象通过.metadata.namespace指定Namespace,如果没有指定Namespace,那么就是归属于默认Namespace default。 Name 名称是一个重要的属性,是人类可读的,元数据中的.metadata.name用于指定API对象的名称。Kubernetes系统中的API对象必须能够通过名称唯一标识,Kubernetes包含Namespace的逻辑层级,大部分API对象必须归属于Namespace,所以这些API对象的名称必须在Namespace内唯一。而另外对于Node和Namespace来说,需要在Kubernetes系统中唯一。 Label Label用于区分API对象的Key/Value对,Label存放的应该是具有标识性的数据,Kubernetes通过Label可以对API对象进行选择。ReplicationController和Service都是通过Label关联Pod,而Pod也可以通过Label选择Node。 Annotation Annotation用于存放用户的自定义数据,Annotation存放的是非标识的数据,所以不能像Label一样进行对象选择。但是Annotation的数据可以是长数据,可以有结构或者无结构,作为Label的一种补充,Annotation也是Key/Value对。 ServiceMesh相关 这部分不谈sidecar,也不谈Envoy和Dubbo的区别,具体可以看上面提到的两篇文章。个人认为,ServiceMesh最核心的点还是控制面的能力,也就是Istio相关的事情。Istio如果model定义得足够标准且可扩展,以至于成为像Kubernetes成为事实上的容器应用的PaaS标准,那么无论Dubbo,还是envoy,还是Nginx,还是Conduit都会作为一种可选的方式来接入,就像用户(一般是开发者)只看到的是Kubernetes,而无需关注Docker还是其他CRI的实现。 所以,下面着重把istio定义的一些model来做个记录。 Istio的设计第一要务 一句话理解就是先做service的基本模型抽象,继而允许适配到具体的容器调度平台上,无论pilot还是mixer。 Istio的模型 Istio Services Model Introduction Describes the abstract model of services (and their instances) as represented in Istio. Independent of the underlying platform. Platform specific adapters found populate the model object with various fields, from the metadata found in the platform. The platform independent proxy code uses the representation in the model to generate the configuration files the proxies. The proxy code is specific to individual proxy implementations Service is a unit of an application with a unique name that other services refer to. Service instances are pods/VMs/containers that implement the service. There are multiple versions of a service. Services Model: Services Each service has a fully qualified domain name (FQDN) and one or more ports where the service is listening for connections. A service can have a single load balancer/virtual IP address associated with it, such that the DNS queries for the FQDN resolves to the virtual IP address (optional). In Kubernetes, a service foo is associated with foo.default.svc.cluster.localhostname, has a virtual IP of 10.0.1.1 and listens on ports 80, 8080 Services Model: Instances Each service has one or more instances, i.e., actual manifestations of the service. An Instance represents an entity such as a pod. For example, a nodeJs backend Service called catalog with hostname (catalogservice.mystore.com), running on port 8080, with 10 pods (Instances) representing the service. Each Instance has a network endpoint: IP:Port. Services Model: Service Versions Each version of a service can be differentiated by a unique set of labels associated with the version. Labels are simple key value pairs assigned to the instances of a particular service version. All instances of same version must have same tag Istio expects that the underlying platform to provide a service registry and service discovery mechanism. Services Model: Labels Labels partition Instances into subsets. For example, grouping pods by labels ”app=reviews,version=v2", will give all v2 instances of Service: reviews.example.com In the absence of a multiple Versions, each Service has a default Version that consists of all its Instances. Services Model: Routing • Services have no knowledge of different versions of the Service being accessed. • Services are simply accessed using the Network Endpoint of the Service. • The proxy is responsible for routing the connection/request to the appropriate Versionbased on the routing rules set up by the user and pushed to Istio-Manager. 云原生的自问自答 何为云原生?当前各有理解,但基本是围绕CNCF的定义来展开。CNCF做了这样的定义: 云原生技术有利于各组织在公有云、私有云和混合云等新型动态环境中,构建和运行可弹性扩展的应用。云原生的代表技术包括容器、服务网格、微服务、不可变基础设施和声明式API。 这些技术能够构建容错性好、易于管理和便于观察的松耦合系统。结合可靠的自动化手段,云原生技术使工程师能够轻松地对系统作出频繁和可预测的重大变更。 云原生计算基金会(CNCF)致力于培育和维护一个厂商中立的开源生态系统,来推广云原生技术。我们通过将最前沿的模式民主化,让这些创新为大众所用。 云原生是核心价值和意义在哪里?我们能做什么才不会被Google,Redhat,Pivital等“意识形态”所带偏?狭义上的云原生是以Kubernetes为基石,那传统的微服务方案是要如何改造来融入? 前面一个问题估计就是个人云亦云,很可能被人带着走的,走入被人牵着鼻子的境地。云原生首先是一种思想,而Kubernetes是提出这套思想的CNCF所推崇的最佳实践和核心,无论是CNCF的Landscape还是Trail Map都能看出Kubernetes的无处不在,所以在我们无法准确表述云原生思想和没有自己的服务云原生的实现情况下,从狭义上理解Kubernetes≈≈云原生,通过应用和实践Kubernetes来慢慢理云原生思想无疑是最合适。 传统意义上的微服务方案Dubbo/HSF融合到CNCF Landscape,成为其一等公民,从而享受到这个生态的服务红利,同时也要展示出自己的特色,有其独特的吸引力来立足,说白了让大家知道Dubbo也是先进的“时髦”的微服务方案,这个问题才是核心,但回答这个问题,好难!期待有人能答疑解惑。 简单说来,云原生(Cloud Native)是伴随的容器技术发展出现的的一个词,需要应用符合一些特质如无状态、可持续交付、微服务化等。Kubernetes与云原生概念两者互相成就,一起推波助澜,基本上形成了当前新一代PaaS的基石。云原生的流行本质上是开放开源的流行:开放意外着可扩展且共建,开源意味着可控且快速迭代。 从一定角度看,以Kubernetes为基石来落ServiceMesh最终拥抱云原生这条路应该是比较切实际。
开头 Service Mesh这个“热”词是2016年9月被“造”出来,而今年2018年更是被称为service Mesh的关键之年,各家大公司都希望能在这个思潮下领先一步。今天我也分享阿里中间件在这方面的观点,思考和实践。考虑到有些人没了解过Dubbo(集团内以HSF为主)和Servicemesh,先简单介绍下这两个词。Dubbo应该是国内最受欢迎的远程服务框架,在Github上有超过2w的star数,也是阿里分布式架构互联互通的核心所在。跟Dubbo一样,servicemesh也是面向服务互联互通这一问题域,是云原生技术栈的核心之一;大家可以简单理解service mesh就是云原生组织定义的微服务架构解决理念。Dubbo是实现框架,融入servcemesh理念就是我们今天分享的。 现状和挑战 当前Dubbo支撑的阿里分布式应用内支撑万级别的应用数,运行在20多万的服务器实例上,每天调用量是万亿级别,这应该是国内最大的分布式应用集群。 挑战主要来自三方面 首先, 数以万计的应用意味着有以十万级的服务,理顺错综复杂的服务拓扑关系,甚至及时诊断某个异常调用链路,需要考虑海量数据的拉取分析,是非常有挑战的,阿里通过EagleEye鹰眼链路系统提供可观察性和治理能力来解决;第二个挑战是机房级别容灾,阿里的机房是分布在天南海北,大家可以想象横跨数千公里的网络延迟会造成服务互通很大的影响,所以在保证一定恢复时间和一定数据容错的情况下做异地多活是有巨大挑战,阿里通过支持异地多活的单元化架构解决。第三个挑战是阿里业务众多,尤其像阿里生态中的高德,UC,优酷等所使用的开发语言跟淘系Java是不一样的,比如PHP,C,Nodejs,Dart等,要维护多个版本并保证各版本具有同样的功能是成本比较高的;这个挑战在云原生的新一代理念下更具挑战,毕竟。今天主题跟第三个挑战是息息相关,能解决一定的问题。 这里讲个大鱼吃小鱼的故事来简单理解下云原生:软件会吃掉这个世界,也就是信息化不可避免,而开源会吃掉软件,最终云原生会吃掉开源。这正代表了云原生理念的颠覆性,从商业软件到开源到云原生,环环相套,以体系化和层次化的方式推荐各个方面的开源方案和标准,这会极大降低企业级架构服务的技术门槛,是企业信息化之路的一大利好,当然也是进化方向。这个故事跟今天的主题--开发者定义软件未来,是非常契合,也就是说这个趋势至少在企业级软件服务领域正在发生。云原生:Cloud Native is Patterns with A complete and trusted tool kit for modern architectures。 Service Mesh的典型方案 Service Mesh的典型方案 讲完故事,回到servicemesh。 传统形态下SDK代表着一个特定语言的库,由应用和微服务框架共处一进程内,在发布升级中共享生命周期。比较典型的代表是Twitter的finagle,Google的stubby/grpc,阿里巴巴的HSF/Dubbo. Serviemesh下推荐是右边Sidecar方案,Sidecar方案没有引入新的功能,只是改变了原有功能的位置,以独立的应用来存在,大家可以暂时以nginx来理解其网络代理能力也可以。 在这张图中希望大家关注两个信息, 1)所有的sidecar形成逻辑网络被称为数据面,是业务服务的链路中是强依赖节点,承载了业务数据互联互通的基础;传统的ops管控服务被称为控制面,这部分跟传统是大同小异。 2)在sidecar形态下,网络会增加两跳,即应用与sidecar之间,他们之间的数据互通也是基于协议规范。后面会详细讲。 Sidecar模式的优劣 接下来从开发和运维两个阶段来分开比较。 多语言支持方面,既然sidecar是独立应用,用最合适的一种语言开发完成即可,就避免了需要针对不同语言的应用场景做不同的版本开发。当前阿里选择基于C语言的Envoy做二次开发来追求最小的footprint和性能,当然也曾经历一些弯路,比如曾经用Java开发过一个sidecar,但最终由于引入JRE体量大和GC带来的抖动等问题证明不可行。有必要强调的是:这里说的是sidecar自身开发现在避免了多语言多版本的问题,而真要支持任意服务自由采用任意语言实现这一理想,是需要站在从业务到数据面再到业务的整个链路上的数据交互做思考。性能方面,sidecar情形下由于会增加两跳,这两跳是业务应用与sidecar的两个进程之间的调用,这是本机,即便是经过优化,也是会增加进程切换以及数据转换的开销。经过我们的优化测试,在正常的业务访问下,相比SDK形态下最多增加1毫秒的开销,这在大多数业务情形下是基本无感知无影响。再看运维阶段的比较,一般SDK形态的服务框架都是只关心开发的诉求,对于如何运维都是不关心,而软件生命周期中运维是最长的,如何从中间件角度解决更多的运维问题是非常有意义的。阿里的中间件经常需要升级,以库的形式升级时就需要业务方应用重新打包,这个推动业务方变更的方式是比较被动,而且周期很长。 当以镜像为基本原子单位进行发布部署时,阿里的中间件SDK体量大概是200兆,需要与业务一起打包,这样在业务应用升级时让分发的包就显得笨重,时效性相比sidecar形态就差一截。稍微总结下,sidecar具有两个明显优势,一个是多语言开发维护成本低 ,另一个是独立升级,当然代价是需要增加一点点的网络延迟。至此大家是不是觉得Sidecar基本完美? 别着急,需要大家再思考一个问题:SDK模式下中间件组件会随应用一起发布,拥有完全一致的生命周期;而在sidecar模式下,如何管理sidecar的生命周期?这里可以拿无线耳机来举个例子,无线耳机是独立了,但必须独立电源的驱动,所以充电是要的。是的,在大规模的集群中这个点会带来不小的复杂性。 关键点 下面跟大家分享下我们对servicemesh理解的三个关键技术点。分别是sidecar运维,数据面与控制面的集成,协议。 先说sidecar的运维,这是个难点,也是为什么sidecar方案以前没有被广泛应用的重要原因。前面说sidecar与应用现在成为两个不同的进程,要考虑多个事宜,一是要考虑如何把sidecar与应用部署在一起,二是考虑业务进程或sidecar进程一方需要升级重启时如何协同来保证请求的正常处理或转发,即优雅上下线的问题。这些事宜考虑清楚并解决后,算是具备servicemesh的前提条件。当然,kubernetes解决了这块的事情,提供了initiator类似插件的机制来对原子性的pod进行注入sidecar,并通过健康检查机制来保证两个进程的协同。简单地也可以这么理解:先把kubernetes容器调度平台的实施是servicemesh的前提条件。数据面中的sidecar的服务治理能力则是其核心竞争力,包括负载均衡策略,路由,安全,权重等等,这些能力是以规则形式通过控制面来统一下发给数据面。在传统微服务框架下数据面和控制面的集成是紧耦合,也就是数据面和控制面是一体的,举例来说用了Dubbo框架,只能选择Dubbo-Ops。而Envoy作为servicemesh思潮的带领者,提出了一整套的API规范,Istio可以实现其xDS接口,阿里巴巴也可以根据自己的架构设计实现类似的服务平台。协议 协议 协议, 重要的事说三遍。。。sidecar和Dubbo的内核是网络协议的处理器,而sidecar又是面向多语言场景的,所以自然协议处理能力是要强调的。先说下阿里Dubbo当下向Mesh方向发展时遇到难点。首先我们的服务接口都是通过Java Interface描述,其次涉及的传输模型DTO也是Java POJO定义,最后协议也是私有的。这会导致跨语言比较难,而sidecar形态需要面向多语言,这些问题更是首当其冲。考虑到这里有点稍微偏细节点,希望大家带着如下问题来先思考下:业务应用到sidecar之间的数据交换要考虑什么? Sidecar自身在处理网络字节流时又要考虑什么?是的,首先业务应用最好都不依赖特定协议库,也不依赖特接口定义库;Sidecar自身处理数据时跟nginx很接近,但最好具备协议转换适配的能力,比如把基于HTTP的请求转换为Dubbo请求,就能轻松集成Dubbo遗留系统。 回看协议 既然协议在跨语言场景下如此重要,有必要稍微回归下协议的历史轨迹。看历史一般是轻松有趣的过程,最重要的好处是能使我们头脑清晰而不迷茫。 我们先从2008年说起,很近也就10年,阿里服务框架诞生这一年。当年各大公司还在炒作SOA思想的时候,阿里在不清楚SOA思想的情况下根据自身业务诉求实践拥抱了SOA的架构。阿里服务框架一直是从三个层面来定义,第一RPC通信 第二是提供丰富强大的治理能力 第三就是基于容器隔离的运维能力,使得中间件可以独立升级。这个理念直到今日都是非常先进,非常的赞。就像前面说的,Dubbo主要是面向Java领域的微服务架构解决方案,在以Java为主导的技术架构下是绝对首选,但因为其协议设计是私有特性,要想成为跨语言的协议标准是有一定难度。 事实上,之前已经出现了很多通用的跨语言的服务集成规范。最早是91年的考吧,是分布式对象访问协议,2000年的SOAP是当年webservice思想下的协议,无论是考吧还是SOAP都是支持所有平台和语言的一套规范,但是设计地比较复杂笨重,且性能存在一定问题。 REST是一种架构风格,相比SOAP的设计,有非常优秀的理念和最佳实践指导,并且万维网作为世界上最大型最成功的的分布式应用是REST最好的证明。但跟SOAP一样,REST跑在1上有性能瓶颈,这个也可能是当年阿里服务框架没有选择REST规范的原因。额外提下,REST思想虽然很早就有,但事实上REST的规范在Java领域JAX-RS API 直到最近两年在2.2版本下才算稳定成形,且越来越接近微服务框架。 1996年的1在连接通道不支持多工复用,根本无法发挥TCP/UDP的网络能力;而到了2015年HTTP2则解决这些,能够最大限度的利用TCP层的网络宽带,且支持了streaming,push等交互模式,这些跟很多的私有或专有应用协议干得是一个事,但是标准化的大家都容易接受的事。这里必须提一下,伴随HTTP2而来的是grpc,原先Google早早推出了Protocolbuffer,但一直没把自家stubby开源,我猜测最大的原因是不想grpc跑在一个私有协议上,而是在等HTTP2. 总结下来,协议技术一直在向着轻量级和标准规范化的方向发展。像SOAP,考吧这些重量级的不跨平台的协议必然消失在历史车轮里,私有或专有的协议也会向标准协议靠拢。在面向跨语言的场景下,有两种的协议规范是大概率胜出,一种是REST,一种是grpc,两者都是以HTTP为交换通道。 面向多语言协议的三层面 展开来讲,在面向多语言的协议需要考虑三个层面。 先从最右边的会话层,干得事是在tcp字节流的基础上形成交互模式,比如 一对一的标准请求响应模式, 以及onway, 一对多的streaming模式。Dubbo在这一层是有扩展能力的,目前除了支持自定义的Dubbo-Remoting,也支持基于HTTP通道能力,我们觉得未来的趋势是HTTP2,所以也会支持这块.这里在分享一句话跟大家一起思考,HTTP不是RPC,HTTP被翻译成超文本传输协议,但不是传输层。另外提一下,这一层是对于MQ,Streaming Compute,Cache等等都是通用的。 再说展示层,干得事是在真正的服务调用过程中,业务对象以何种形式被格式化,比如HTTP头中的content-type就用于这个展示协议的描述,最常用的JSON,TXT,XML等。这一层对于sidecar来说,可以做透明处理,也就是说sidecar只需要解析出头部信息,前提是要求业务应用把需要在治理时用到的一些字段信息以字符串形式放到头部中。Dubbo当前是默认HEssion,跨语言能力比较弱,所以未来JSON是我们首选。 最后,首先一个服务是干什么的,它的名字,方法,参数都是怎样的,等等基本元信息是需要统一描述的,即便像是REST这样基于URI,也是需要一种协议来定义,以前Dubbo是基于java interface来定义,现在我们在多语言的mesh环境下是考虑向OpenAPI specification方向考虑,支持swagger。 我们相信在这几个层面,尤其是会话层和应用层,用不多几年一定会是标准化的,尤其是在云原生的趋势下。 方案之Kubernetes集成Du bbo Mesh方案之Kubernetes集成其实,servicemesh在最近两年流行最大的原因是云原生理念的逐渐深入人心,从广义角度看,能够融入云原生的微服务框架都能称得上servicemesh。谈云原生,肯定绕不开kubernetes,所以我们在Dubbo Mesh的方案的第一个分享是 在kubernetes下的集成,目标是复用Kubernetes的基础服务,从而使得Dubbo能解决kubernetes环境下的微服务集成问题,同时能最大限度的利用dubbo已有的功能。核心思路是两点, Dubbo应用在构建阶段自动生成其deployment和service的声明文件。这个主要是解决Dubbo与kubernetes的服务映射。Dubbo地址注册针对kubernetes的扩展实现,通过Kubernetes的APIServer来拉取并监听某个服务的podIP。这样,在kubernetes集群内,Dubbo服务就能在其podID的虚拟网络内实现服务发现。前面讲了很多关于协议方面的东西,也为我们在Dubbo Mesh的方案的第二点分享是做了铺垫, 第二点的目标是Dubbo 协议的多语言支持。核心思路是 方案之跨语言协议支持 积极兼容开源社区Envoy,这个使得Envoy上兼容支持Dubbo的私有协议。Dubbo支持HTTP/2作为传输通道,这个是为了Dubbo的协议通道能力向更加开放更加标准规范的方向做努力。 ServiceMesh之云原生的指导 孤立地看待servicemesh其实和传统服务框架,价值还不算大,甚至成本相对更高。这时候,当我们把servicemesh设定到云原生的上下文中,就会发现不一样的意义。 servicemesh是云原生理念的路径地图的第五步,如果没有前面的容器化,CICD等四部,真正拥抱servicemesh也只是空中楼阁。阿里在这方面的实践经验是,servicemesh的实施是需要结合软件开发的整个生命周期进行统筹,从软件在本地开发测试,到通过持续集成服务的自动化构建,再到以镜像方式分发到仓库并依托调度云平台的持续部署,最后持续监控。 dubbo已经开源好多年,是非常符合云原生这个原则,正向servicemesh方向和云原生理念上努力,为企业信息化做出一点贡献。 总结 总结一下Dubbo Mesh是Dubbo在cloud native下的一种演进,这个演进是为了更加开放更加靠近标准协议规范的方向做的探索。通过分享希望大家能带走三点思考。 servicemesh的多语言方案其实是走规范化标准化的协议之路,这样才能覆盖多语言的诉求。 建议大家根据实际业务场景来慎重权衡sidecar模式下运维复杂性和收益回报。 一定把servicemesh设定在云原生的上下文中才具意义,离开了Kubernetes谈servicemesh的实践是不建议的大跃进。 最后希望大家一起共建共享的Dubbo开源社区,谢谢。