[
  {
    "path": ".gitignore",
    "content": "book\n.DS_Store\n"
  },
  {
    "path": "README.md",
    "content": "# 从0到1实战微服务架构(开源电子书)\n\n## 前言\n\n微服务是继SOA后，最流行的服务架构风格之一。\n\n按照微服务对系统进行拆分后，每个服务的业务逻辑都更加简单、清晰。服务之间是松耦合的，模块之间的边界也更加清晰。\n\n微服务有效降低了软件项目的业务复杂程度，为小团队独立开发、持续交付和部署打下了良好的基础。\n\n遗憾的是，微服务并不是银弹。与传统的单一架构相比，微服务架构对团队的组织架构、技术水平、运维能力等方面，都提出了更高的要求。如果没有掌握得当的方法而生搬硬套，微服务架构只会会适得其反－－降低项目的开发效率，这是本书的创作初衷之一。\n\n在国内外的技术社区中，比较推崇现有开源方案，如\"Spring Cloud全家桶\"或者阿里开源的\"Dubbo\"。\n\n上述框架通常已经实现了服务发现、配置、负载均衡、限流熔断，等微服务架构所必须的的核心功能。\n\n使用开源框架省却了造轮子的过程，但也降低了我们学习、思考的动力。\n\n为什么需要服务发现，又如何实现它呢？配置中心呢....思考和设计的过程充满了挑战，也是提升自身架构能力的一种手段。这是本书的创作初衷之二。\n\n已有的微服务资料过于重视微服务的开发，忽略了微服务赖以生存的生态系统：工具链、自动化运维。可以说，离开了这两点的支持，微服务架构将难以落地。完善这两方面的思考和实战，是本书的创作初衷之三。\n\n为此，我撰写了这本《从0到1实战微服务架构》。让我们\"暂时忘掉\"已有的、成熟的开源解决方案。尝试亲自动手，实现微服务架构的各个模块。\n\n我们会从微服务开发、工具链、运维这三个角度，阐述微服务架构的实战方案。\n\n如果本书帮助了你，欢迎在在[github](https://github.com/liheyuan/hands-on-microservices)加Star，但严禁用于商业用途！(参见本页底部版权声明)\n\n由于能力水平所限，本书难免存在各种错误，恳请各位进行指正(Issue or PR)，谢谢！\n\n## 2.0前言\n\n自从本书发布了1.0版本后，已过去2年多了。\n\n技术圈又发生了很多变化，与本书密切相关的有：\n\n* Spring Boot 2.0 稳定版发布\n* Kubernetes下的包管理项目“Helm”，正式加入CNCF基金会\n\n为此，我开启了本书2.0版的写作计划。\n\n由于gitbook项目已不在维护，我们改用改用[mdbook](https://github.com/rust-lang/mdBook)做为图书渲染工具。\n\n本书的写作工具为[MarkText](https://github.com/marktext/marktext)。\n\n写作水平有限，还请各位多提宝贵意见。\n\n## 读者基础\n\n由于篇幅、精力所限，本书无法写成一本”零起点”教程。我假设读者具有至少2年的服务端工作经验，并且了解以下技术或原理：\n\n* Git\n* Maven & Gradle\n* Docker & Kubernetes\n* Java\n* Spring / Spring Boot \n* 数据库: 如MySQL\n* 消息队列: 如RabbitMQ\n* 缓存系统: 如Memcached \n* 内存数据库: 如Redis\n\n本书可以供架构师、项目经理、高级服务端程序员参考、学习。\n\n动手实战是本书的核心内容，因此本书所涉及的全部代码，都托管到了我的[Github上](https://github.com/liheyuan)(以lmsia-开头的项目)。\n\n这些代码以研讨为主要目的，也可以直接应用于生产，但本人不对其稳定性负责。\n\n## 版权\n\n本书虽然在github上公开写作，但版权归本人[Coder4](https://coder4.com)所有。\n\n依照 [署名-非商业性使用-相同方式共享](https://creativecommons.org/licenses/by-nc-sa/2.5/cn/) ，任何人可以在保留署名的情况下转载。但严禁用于商业用途。\n\nThis is a book powered by [mdBook](https://github.com/rust-lang/mdBook).\n"
  },
  {
    "path": "book.toml",
    "content": "[book]\nauthors = [\"lihy\"]\nlanguage = \"en\"\nmultilingual = false\nsrc = \"src\"\ntitle = \"从0到1实战微服务架构(第2版)\"\n"
  },
  {
    "path": "legacy/SUMMARY.md",
    "content": "# Summary\n\n* [从0到1实战微服务架构](README.md)\n\n* [架构概览](architecture/README.md)\n    * [微服务架构概览](architecture/overview.md)\n    * [运维技术链概览](architecture/devops.md)\n    * [微服务技术栈概览](architecture/microservics.md)\n    * [研发工具链概览](architecture/toolchain.md)\n    \n* [Kubernetes快速入门](k8s/README.md)\n  * [集装箱、容器化、容器编排](k8s/docker-k8s.md)\n    * [Kubernetes 快速入门](k8s/k8s-intro.md)\n    * [搭建Kubernetes集群](k8s/k8s-cluster.md)\n    * [为Kubernetes集群开启ipvs](k8s/k8s-ipvs.md)\n    * [使用Helm进行包管理](k8s/helm.md)\n    * [办公网与Kubernetes集群的打通](k8s/k8s-office.md)\n    * [Kubernetes集群的高可用方案](k8s/k8s-ha.md)\n  \n* [微服务的自动发现与负载均衡](ms-discovery/README.md)\n\n    * [微服务的自动发现与负载均衡](ms-discovery/msd.md)\n\n    * 使用边车模式于微服务部署\n\n* [微服务的开发框架](spring-boot/README.md)\n  \n    * [Gradle子项目划分与微服务的代码结构](spring-boot/sb-gradle-structure.md)\n    * [Spring Boot整合Thrift RPC](spring-boot/sb-thrift.md)\n    * [Spring Boot整合REST服务](spring-boot/sb-rest.md)\n    * [Mockito 单元测试打桩神器](spring-boot/sb-mockito.md)\n    \n* [微服务的存储与缓存](ms-storage/README.md)\n    * [MySQL 数据库的运维](ms-storage/mysql-devops.md)\n    * [Spring Boot整合MySQL](ms-storage/sb-mysql.md)\n    * [Memcached 缓存服务的运维](ms-storage/memcached-devops.md)\n    * [Spring Boot整合Memcached](ms-storage/sb-memcached.md)\n    * [Redis 内存数据库的运维](ms-storage/redis-devops.md)\n    * [Spring Boot整合Redis](ms-storage/sb-redis.md)\n    \n* [微服务的消息队列](ms-msgq/README.md)\n    * [RabbitMQ 消息队列的运维](ms-msgq/rabbitmq-devops.md)\n    * [Spring Boot整合RabbitMQ](ms-msgq/sb-rabitmq.md)\n    * [RocketMQ 消息队列的运维](ms-msgq/rocketmq-devops.md)\n    * [Spring Boot整合RocketMQ](ms-msgq/sb-rocketmq.md)\n    * [Kafka 流处理平台的运维](ms-msgq/kafka-devops.md)\n    * [Kafka 流处理开发简介](ms-msgq/dev-kafka.md)\n    \n* [微服务日志监控](ms-log/README.md)\n    * [Spring Boot配置Logback日志](ms-log/sb-logback.md)\n    * [Spring Boot整合分布式追踪](ms-log/sb-trace.md)\n    * [ELK日志分析平台的运维](ms-log/elk-devops.md)\n    * [Spring Boot整合EBLK日志分析平台](ms-log/sb-eblk.md)\n    \n* [微服务平台监控](ms-monitor/README.md)\n    * [Sentry 错误预警系统的运维](ms-monitor/sentry-devops.md)\n    * [Spring Boot整合Sentry](ms-monitor/sb-sentry.md)\n    * [Kubernetes + Prometheus + Grafana平台监控](ms-monitor/k8s-prometheus-grafana.md)\n    \n* [微服务配置中心](ms-config/README.md)\n    * [cfg4j及方案简介](ms-config/cfg4j.md)\n    * [Spring Boot整合配置中心](ms-config/sb-config.md)\n    \n* [微服务熔断与限流](ms-circuit-breaker-and-limit/README.md)\n    * [熔断与Hystrix](ms-circuit-breaker-and-limit/sb-hystrix.md)\n    * [限流的实现](ms-circuit-breaker-and-limit/sb-limit.md)\n    \n* [微服务持续交付](ms-delivery/README.md)\n    * [Jenkins平台的运维](ms-delivery/jenkins-devops.md)\n    * [Jenkins持续集成](ms-delivery/ms-ci.md)\n    * [Jenkins持续部署](ms-delivery/ms-cd.md)\n    \n* [研发工具链](toolchain/README.md)\n    * [LDAP 内部账号管理系统](toolchain/ldap.md)\n    * [gerrit 代码的版本管理与评审](toolchain/gerrit.md)\n    * [Nexus 私有maven仓库](toolchain/nexus.md)\n    * [BOM 减少版本冲突](toolchain/bom.md)\n    * [Spring Boot 项目模板](toolchain/spring-boot-template.md)\n    * [开发效率脚本](toolchain/spring-boot-scripts.md)\n    * [打压工具](toolchain/stress-test.md)\n    \n* [运维工具链](devops/README.md)\n    * [Docker 私有仓库](devops/docker-repo.md)\n    * [Nginx REST 网关自动配置](devops/discovery.md)\n    * \n    * [OpenVPN访问Kubernetes集群内网](devops/openvpn-k8s.md)\n    * [线上跳板机](devops/jump-server.md)\n\n"
  },
  {
    "path": "legacy/architecture/README.md",
    "content": "# 架构概览\n\n当我们谈微服务架构时，我们需要关注哪些点，本章将从这里说起。在了解了微服务的整体架构后，我们会从“研发工具链”、“微服务技术栈”、“运维技术链条”三个角度展开。我们会讨论微服务架构中，如何对这三类问题进行技术选型以及做出这些选型决策的原因。同时，我们将探讨如何将这三部分有机地融入到微服务架构中。\n\n"
  },
  {
    "path": "legacy/architecture/devops.md",
    "content": "# 运维工具链概览\n\n在看过微服务整体架构后，我们来讨论下架构的各个层次中，本书所选用的技术栈。\n\n与前面类似，我们依然自底向上讨论。\n\n运维工具链选型\n* 基础设施层：对于绝大多数的中小公司，且无强烈的数据保密需求，我强烈建议使用云主机。\n * 运维成本更低。想对机器加64GB的内存，如果自运维的话跑去机房、断电、拆机器、插内存，半天过去了。如果采用云主机，就是点点鼠标，重启一下的事情。\n * 弹性更大。老板一拍脑袋，明天要上线大促，预估要扩容10倍。只有一天时间，是不可能完成100台机器的采购、上架、配置的。大促结束后，流量回归到平常情况，这100台机器还要继续空转烧钱么？如果采用云主机，这些就是点点鼠标，甚至是几个API调用就可以搞定的事情。\n * 平均稳定性更高。内存ECC故障、硬盘坏道、RAID卡故障，这些都是自运维服务器时代的家常便饭。若采用云主机，就可以很好的避免这些烦恼。常见的大厂云主机，都提供了至少3个9的在线时间保障，与运维服务器相比，平均稳定性更高。\n* 运维平台层之容器管理：前面已经提到，微服务需要借助容器技术才能“顺利启航”。目前主流的方案有：docker加脚本、swarm集群、Kubernetes集群、Marathon集群。本书选用Kubernetes集群，它内置的功能完全可以满足”容器的监控、调度、管理“。在这里我们采用排除法，说说为什么不选用其他几个方案。\n * docker加脚本：2014年Docker刚刚兴起时，还没有集群管理的解决方案，多数公司采用了这类架构，在不同物理机器上，通过自动化脚本来启动不同的容器。这种方式简单直接，但是当集群规模扩大后，运维非常困难。此外，这种方式很难解决自动网络配置、自动调度、容器数据迁移等很现实的问题。\n * swarm集群：swarm集群方案是Docker公司于2014年末推出的容器集群技术方案。尽管swarm是Docker公司的“亲儿子”又是市场的先发产品，但swarm很快被后起之秀Kubernetes超越。时至今日，从的新功能跟进、社区活跃、文档完善程度等方面，都弱于Kubernetes。更重要的是，借助CNCF基金会的影响力，Kubernetes已经成为了事实上的容器集群解决方案，GCE、AWS等云平台，都原生支持Kubernetes集群。在2017年，Docker公司在自己的商业化产品中直接集成了Kubernetes，标志着swarm项目的全面落败。所以在本书中，我们也不会选用swarm集群。\n * Marathon集群：Marathon是构建在Mesos集群上的一套容器集群管理软件。想使用Marathon,先要部署一套底层的Mesos。对于大型公司，特别是已经部署了Mesos的Hadoop集群的公司来说，这种搞法可以提升机器复用程度。但对于中小规模的企业而言，Mesos + Marathon多少有一些“”高射炮打蚊子“的感觉。此外，虽然Marathon在大规模机器管理上有比较多的经验，但并未在容器编排上投入太多新功能，更像是借助容器的概念为自己\"造势\"。综上所述，我们也不会选用Marathon集群。\n* 运维平台之持续部署系统：部署前需要先构建，本书的微服务开发选用Spring Boot框架，在构建方面，我们使用Gradle(之后会阐述原因)。在持续部署方面，我们选用老牌工具Jenkins，通过一些插件和配置完成全套的持续部署。\n* 运维平台之部署版本管理系统：前面已经提到，我们将采用容器技术。本书采用自建私有Docker仓库的方式，完成容器的镜像工作，并使用它作为部署版本的管理系统。\n\n本书的主线是微服务的架构及开发。为了保证这一主题的的稳定和连惯性，我们将上述运维工具链的使用单独抽提出来，在[《运维工具链》](../devops/README.md)一章中介绍上述内容。\n\n"
  },
  {
    "path": "legacy/architecture/microservics.md",
    "content": "# 微服务技术栈概览\n\n下面来看一下微服务相关的技术选型\n\n* 服务开发框架：在微服务开发方面，我们选用Java作为开发语言。市面上的语言众多，特别近几年来，Go、Rust语言作为服务端语言快速崛起。既然如此，我们为什么还要选用基于Java语言呢？尽管Go等语言崛起的很快，但相对依然小众，能够熟练运用这些语言进行生产开发的人才更是少之又少，从人才供给量上就无法满足业务需求。此外，新兴语言的社区相对活跃，版本变化较为频繁，经常出现各种不兼容升级，各种坑，这些都不利于业务的开发。反观Java，虽然大家都批评多年来一直是”八股文“，但一直保持向下兼容，且依然稳重求进。最终要的是，Java拥有”海量“的开发人员基础，人才供给不会成为瓶颈。因此，我们选用Java作为开发语言，并使用Spring Boot作为开发框架。Spring Boot作为Spring框架的一次\"革命\"，可以说是为了微服务而生的，在本书的后续章节，大家会逐渐体会到选用Spring Boot的原因。\n* 服务注册与发现：为了简化实现难度，我们将借助Kubernetes内置的服务和虚拟端口功能，来实现服务的注册与发现。换句话说，我们将服务注册与发现的能力，下推一层到运维平台层。对于微服务这一层，服务的注册与发现是完全透明的。\n* 熔断与限流：微服务之间的调用链更加复杂，为了降低＆隔离服务故障造成的影响，一般选用和熔断或限流策略。我们选用Hystrix作为熔断功能的基础库并进行了封装，Guava的RateLimit作为限流功能的基础库并进行了封装。\n* 配置中心：这里主要关注配置的自动下发、自动更新和易维护性。我们采用git + cfg4j的模式实现配置中心。\n* 后端组件\n * RPC:目前主流的开源RPC框架是gRPC和Thrift。gRPC在设计理念上更为先进，且原生支持HTTP2，可以直接集成到客户端上。Thrift作为老牌RPC框架，历经多家公司的考验，已经非常成熟和稳定，且性能比gRPC更为出色。因此，我们选用Thrift作为本书的RPC框架。\n * 关系型数据库：我们选用市场占有率最高的开源数据库MySQL。Spring Boot内置了多种数据库接入方式，我们会采用JPA和“裸写”DAO两种接入方法，以满足不同应用场景。\n * 内存数据库：”让系统更快“是我们不断追求的目标。当基于磁盘的MySQL数据库不能满足性能需求时，我们选用Redis内存数据库。Redis是一款高性能的内存数据库，在官方的性能评测中，其QPS可达到十万级别[^1]。在接入组件方面，我们选用Redisson。与老牌的jedis等开源库相比相比，Redisson的优势在于将Redis操作与数据结构进行了有机的结合，可以用类似Java内置数据类型的操作方式轻松地使用Redis。\n * 缓存：构建高性能的分布式系统，缓存是必不可少的。Memcached是经典的高性能分布式内存缓存系统，我们选用它作为后端缓存组件。只有后端组件是不够的，还需要与Spring Boot集成。常见的Java客户端有Spymemcached和xMemcached。由于xMemcached采用了NIO模型，我们选用它作为接入库。\n * 消息队列：当系统的同步阻塞处理频繁出现性能瓶颈，甚至拖垮整个系统时，我们可以引入消息队列，将同步处理转为异步处理。消息队列就是为这种场景而设计的。目前比较主流的开源消息队列有Kafka、RabbitMQ等。从设计理念来看，Kafka是一个成熟的分布式流处理平台，更专注于海量消息和分布式拓展性。RabbitMQ则更加专注于消息队列，且兼容AMQP协议。结合我们的需求，选用RabbitMQ更为合适。虽然Spring内置了AMQP的集成方案，但使用起来略为繁琐。我们会以官方客户端为基础，自行构建一套工具类库。\n* 日志：我们选用Spring Boot默认的logback作为日志记录系统；使用\"EBLK\"组合作为日志收集、分析系统。\n* 监控：\n * 微服务监控：在微服务部署之后，我们需要对微服务和其所在的容器进行的健康状况进行监控。包括容器的内存、CPU、网络状况，以及微服务的GC等信息。我们选用Prometheus作为数据的收集和查询系统，Grafana作为监控可视化平台。我们会探讨如何向Prometheus发送自定义的监控数据。\n * 业务异常报警：微服务监控可以帮助我们了解服务的健康情况，定位性能瓶颈。但对于系统的业务异常却无能为力。Sentry是一款错误追踪系统，可以帮助我们发现并定位逻辑异常。类似的，我们也会探讨如何集成Spring Boot与Sentry。\n\n[^1]: [How fast is Redis?] (https://redis.io/topics/benchmarks)\n"
  },
  {
    "path": "legacy/architecture/ms-arch.xml",
    "content": "<mxfile userAgent=\"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:59.0) Gecko/20100101 Firefox/59.0\" version=\"8.6.0\" editor=\"www.draw.io\" type=\"device\"><diagram id=\"14fd9bf9-1985-7df8-2364-c37e7c9493a5\" name=\"Page-1\">7VtLd6M2FP41WnYOICTEEhzcLtpzek4WbZfYyDYz2HIJnjj99dULg41I7AnCDU1mMXAlQNzvu09hAGfb489lut/8xjJaAM/JjgA+AM/zYBDw/4TkRUlcj0AlWZd5pmWN4DH/h2qho6WHPKNPZxMrxooq358Ll2y3o8vqTJaWJXs+n7ZixflT9+madgSPy7ToSv/Is2qjpAQ5jfwXmq839ZNdR48s0uW3dckOO/084MGV/FPD27S+l57/tEkz9twSwQTAWclYpY62xxkthHJrtanr5j2jp3WXdFddc4GnLvieFgdar1iuq3qpdSHfhor5LoDx8yav6OM+XYrRZw4/l22qbaGHV3lRzFjBSnktXCHxj8ufqpJ9o60RLP/4iF4ALSt67H0J96QazjnKtrQqX/iU+oKQfEHqIk04jLV+nxv0sBMq2aaFXD0v1YRZn27eKI0faL2ZdQinqUMUjKhDf5o69MmIOkQGHeKCPyBe8IN1JV9SCVaMv1Fbu/jvA6sHfnqS/jjiEzx/f2wGT3dJEAjnII5AEoDIBfwlEwKiBMQJSDAIMYgf6ifxRauHnS+Ai1uLukCaI1Cdw3kO247t6AXGWpQW+XrHT5ccMcrlscAz53490gPbPMvEY4z8aRjmDEMI37v0SwY+nGJHmw/eAHwgr/Hhh7EfiEEExHMQOoJBcQxiX3AqDkEMxQHh5HJOiy0N5PnkVOOnwxE5FY7pY7g7iZRHmQHyIIkRCX/z6WxMxPCCS2I4BmJAS8RwTVnQbURwcZ+r4GgL2ANxEAbSeXAu+AJ/HoRCpxf/AVOILKVktTSmEEtCF6uBQHQvUcSGNIwYQPSHANGUhg0DIgLRAwiJNGIPRGTKIMIasntA2J8FvhfCQHja0NUhm/vk173u+81+qvxAtZ3dgx/YGj9knBZFAQZEpneD3Zffjsf5xJdEcA2pYpNFYpkj+G9nBB+cQ9h/28e4PrFEov7SYgCwebJXOxkeM5IQECgDhvQ/sScPoBjVjmg+HVS74R+ZKkbPlnPoT+8H9+ZC4ghohTVzk+X2DUUngdSJnsr4+EyCpwy5j/E1kGPXDuQ1lSxAbjJcEgpnrkJFNPt/IY3hVcaNLBl33VK14rRjhTQSEHIHLkI1ASTREVoljfw0HieLQ5RkvglO4i3gUN3erq/2r+3EDAKnvYKb2yiSDRdpuFHS35v7+Cj6sNOzN6BoKxs37nsMVnAnAjWOZuiDMGzZZChORQjGMii700ETepfBdFybtFd8Y9H4ipA0xblsgrWypTCWLhYLdKP2kKqGkOyhPYgcazJIo06704i071tC2l4ZjWQmlMjciFdAc50JidzZl1XuhFDEwVUourbsNTCgOKASx9l29hznQonQoETsBF0lDrHtbNxnnIISDdsn9pQ44saazAPCqLWf1t1h41HGFY36z401w8aa7xp8lK2NNdhfoA/ODJVAyCSDVwDi4NTzlxJVAXwyo5cZ0BuTGW5HszRb00d9yspqw9ZslxZJI73QQQsCesyrP4X4C9Jnf9UjO76w1pA4rce+0qp60d9vpoeKcVHz3F8Z2+t5aqViea+rmr8NO5TLepbOsqq0XNOqLetCUtIirfLv5/d/l3q7YWwA9bot5Sp92lWh/05t6Ut/Z7l0LZr4xOm0stwLRivM9HUXWj8t5Dog7BXJ3VDY+9XJKFXyarXylsZGZIYXGNnrXJk+FbG1ywDtVck34NkfwT4cnhDfF097tfANeMLp4InubJ+mqnh0PPu38D8cnvjO9ml1t/7NguFOgZTiHmCDcOEMlf93DBWaSkNrwFrbrj+7y3vA7sF6pOA7Bge6wXdUDvj97YH/OAdGCthjcKAbsMflgPtROTBSkB+DA90gPy4HPGtBvvvpVmfHcTLf6EB0zTc6IbIEYn+n44d7vci5+ec1XILkx7S+tGtlzonYeFQH5IrPa2/p/g6RiaHwAjeXBB3cIDJ8EItuB46fNj/RVh2t5ofwMPkX</diagram></mxfile>"
  },
  {
    "path": "legacy/architecture/overview.md",
    "content": "# 微服务架构概览\n\n在正式讨论微服务架构前，有必要用简短的篇幅，讨论下微服务以及这种架构风格的优点和缺点。\n\n在微服务出现之前，我们的架构多数是单体应用架构。会有一个或者少数几个”巨无霸“进程，里面可能包含了”用户管理“、”订单管理“、”支付确认“、”物流“等等各种复杂的业务逻辑和功能。这种传统的单块应用架构风格是很直观、自然的，然而在现代软件开发领域，特别是互联网开发领域中，单块架构遇到了一些问题。\n\n单块架构的缺点\n\n* 耦合严重：单块服务内的各个逻辑之间，往往缺乏清晰的边界。导致内部耦合严重，正所谓“牵一发而动全身”。\n* 维护困难：单快服务包含了过多的业务，代码量严重膨胀。开发人员难免”失焦“，不知道如何下手。\n* 团队协作困难：如果多人同时开发同一个单块应用，势必导致代码冲突成为常态，团队协作成本急剧上升。\n* 测试困难：单块服务是作为一个整体进行开发、上线的。尽管你只对Ａ功能进行了修改，但难免会影响Ｂ功能。随着单块应用的愈发膨胀，测试工作量会提升数倍。\n\n上述单块应用的缺点，在传统软件开发中，尚可通过“小心规划”、“人海战术”等方法解决。但到了互联网时代，就很难实现了。为什么这么讲呢？互联网软件开发，讲究的是“快糙猛”，对迭代速度的要求非常高。对于成熟的互联网企业，一个项目在一天内上线好几次，都是稀松平常的事情。试想一下我们的单块“巨无霸”服务，稍微改动一点，就要经过复杂的代码评审、测试验证，如何才能跟上快节奏的迭代和上线呢？\n\n尽管微服务不是银弹，但微服务的出现，确实从一定程度上改善了这种情况。微服务是⼀种架构⻛格，将单块应⽤划分成⼀组⼩的服务，服务之间相互协作，以组合的形式，实现复杂的业务功能。\n\n微服务架构的优点\n\n* 低耦合：在单块服务中，不同业务的逻辑耦合在一起。做微服务拆分后，微服务内只包含有限的业务逻辑，耦合也随之大大降低。\n* 易维护：微服务内部只包含单一业务逻辑，功能更为集中，更容易开发人员聚焦问题和修改。\n* 适合团队协作：拆分为微服务后，每个服务中涉及的代码和功能更少，可以将不同微服务划分给不同团队甚至个人负责。各司其职后，有效降低了开发和代码冲突，使得其适合团队协作。\n* 测试成本低：在单块服务中，哪怕只改动了一点点代码，也需要对整个巨无霸服务进行测试。微服务拆分后，功能的修改，只需要涉及改动的个别微服务进行测试，有效降低了测试工作量。\n* 易横向拓展：在单块服务时代，巨无霸服务已经占用了大量资源，机器的配置已经很高，若要再单独部署一个结点做负载均衡，成本会非常很高，所以多数情况下，都是通过[纵向拓展](http://blog.51cto.com/linuxgp/764622)的方式提升系统性能（如加内存，换个更好的cpu）。采用微服务拆分后，各个微服务占用的资源更少，可以轻松的通过增加节点的横向拓展方式，提升系统性能。更进一步的说，根据[阿姆达尔定律](https://zh.wikipedia.org/zh-hans/阿姆达尔定律)，微服务拆分后，各个微服务对性能的要求并不一致，可以优先拓展那些具有性能短板的微服务，有效降低了拓展成本。\n* 技术选择更多样：由于微服务是各自独立的进程。各个团队可以根据自己的需要选择不同的技术方案。然而在实践中，我并不推荐这么搞，这会在后面技术选型时展开。\n* 加快迭代速度：上面已经提到，微服务低耦合、易维护、适合团队协作、测试起来成本更低，也更易于横向拓展。采用微服务架构后，可以显著的提升迭代速度。\n\n尽管微服务具有这些优点，我想再次强调：“”微服务不是银弹“”，他解决了单一服务的软件复杂度，提升了迭代速度，但也带来了一些缺点。\n\n微服务架构的缺点\n\n* 运维难度加大：在单块架构中，不管改动多少需求，只需要上线一个服务。而采用微服务后，为了一个需求，可能要上线一堆服务。\n* 开发能力要求高：在单块架构中，巨无霸服务的逻辑都在一个项目中。采用微服务后，逻辑分散在不同的项目中，在加上微服务架构本身引入的新技术，对开发能力提出了更高挑战。\n* 调试难度更大：在单块架构中，我们只需要关注一个或少数几个服务。应用微服务架构后，后端系统演变为分布式系统，可能会出现Ａ调用B，Ｂ调用Ｃ，甚至Ｃ还要调用Ｄ的长调用链，势必增加了调试和问题排查的难度。\n\n幸运的是，通过容器等的新技术的引入，以及合理的架构设计，这些缺点都可以得到一定程度的缓解。本书会在后续章节对这些问题进行展开。\n\n在讨论了微服务的优缺点之后，我们可以看一下微服务的整体架构了。\n\n![微服务整体架构](./ms-arch.png \"微服务整体架构\")\n\n如上图所示，微服务架构可以大致分为五个层次，我们自底向上，逐层做一下解释。\n\n* 基础设施层\n  * 微服务是后端服务，最终一定要部署在基础设施的某台机器上。基础设施层可以是自运维的服务器或机架，也可以选用云计算的虚拟机。\n  * 在这一层，我们重点关注底层“物理资源”[^1]的可用性及调整：计算资源（CPU 和 GPU）、存储资源(内存、硬盘)、网络资源（交换机、路由）。对这些基础设施的维护，或者是直接在机房维护，或者是操作云平台的API。\n  * 举几个例子：大促前预估系统容量不足，我们要加半个机架的机器；直播平台今晚要来大V，预计带宽不足，要增加带宽。这些都是在基础设施层要关注的内容。\n* 运维平台层\n  * 对于后端服务的运维工作，持续交付是最重要的能力。由于微服务数量众多，一般需要构建一个持续交付系统，来完成微服务的自动运维，如微服务的初始化、发布、回滚、扩容。\n  * 采用微服务架构后，上线的次数、频率都会显著提升，这就需要一个上线的镜像版本管理系统，记录上线的镜像版本。在微服务架构中，一般采用容器技术来实现微服务的自动运维。\n  * 容器是轻量级资源，隔离能力相对较弱，我们需要对容器资源进行监控，调度和管理。举个例子：我们有三台物理机，现在物理机Ａ和C各运行了20个容器，物理机Ｂ运行了22个容器，假设每个容器的资源占用完全一致，那么资源调度系统会自动地，将B的两个的容器调整到物理机Ａ和Ｃ上。\n* 微服务设施层\n  * 假设服务A需要调用服务B，那么A服务如何获取服务B的地址(例如IP和端口)呢？在传统的单块架构中，服务数量很少，尚可采用同在配置中写死IP、端口的方法来解决这个问题。但在微服务时代，面对动辄成百上千的微服务，这种做法将不再可行。因此，如何自动注册、发现微服务的多个实例，是架构必须解决的核心问题。\n  * 微服务拆分后，我们的系统从单机演化为分布式系统，为了防止分布式系统的雪崩效应[^2]、微服务设施需要有的熔断和限流的能力。\n  * 面对众多的微服务，逐一修改配置的方式显得更加笨拙，我们需要有一个中央配置平台，快速完成微服务的配置修改。\n  * 前面已经提到，微服务的分布式架构，会加大调试难度，所以日志的收集和预警显得更加重要，同时，我们可以根据业务日志来进行一些监控预警，这和平台层的容器监控预警是不一样的。\n  * 微服务需要集成一些后端组件，如数据库、消息队列、缓存等。\n* 业务服务层：在提供了微服务的基础设施后，我们可以放手开发各个微服务了。业务服务层是一些“基础微服务”或“业务微服务”，他们“各司其职”，服务之间的耦合应当做到最低。\n* 接入网关层：微服务面向的“用户”，一般是Web、移动端、PC端等。出于用户体验的考量，服务端提供给客户端的的接口应当尽量实现结果聚合，减少请求次数。因此，一般要设置一层接入网关，他负责调用业务微服务A B C等，完成结果聚合后，一并返回给客户端。\n\n在绝大多数的公司中，业务规模不会很大，所需要的机器也非常有限，基础设施层往往会使用云平台或机房托管的外包方式完成。因此，本书将不对基础设施层做过多讨论。\n\n[^1]: 由于云主机的虚拟化技术对我们的架构透明，我们也将其看作一种物理资源。\n[^2]: [分布式应用雪崩效用](http://youyu4.iteye.com/blog/2405976)\n"
  },
  {
    "path": "legacy/architecture/toolchain.md",
    "content": "# 研发工具链概览\n\n子曰：“工欲善其事，必先利其器”。前面已经提到，微服务架构的对开发水平提出了更高的＝要求，我们更应该注重研发工具链的建设，以提高开发效率。\n\n* 内部帐号管理：不论大小企业，无论企业性质，都需要一个集中式的帐号管理系统，员工只需要设置一次帐号密码，就可以方便地使用各个不同的系统。我们选用了经典的OpenLDAP 作为帐号管理服务器。OA、代码服务器、Jenkins等系统，都很方便地接入LDAP，实现“一套帐号，各系统共享”。\n* 代码版本管理：从架构和团队协作模式考虑，在微服务架构下，git比svn更合适作为版本管理系统[^1]。GitLab和Gerrit都是经典的Git代码托管系统。GitLab类似于GitHub适合GitFlow的分支独立开发，Gerrit侧重于代码评审。考虑到代码评审的需求较为强烈，我们选用Gerrit。\n* 依赖管理：无论什么开发语言，只要引入了开源库，就需要面对依赖管理的的问题。正如Python的Pip、Ruby的Gem、Node的npm，Java中使用Maven来管理库依赖。对于企业级开发，一般采用自搭建Maven私有仓库的方式，方便内部包的部署和依赖。我们选用Nexus搭建私有仓库，它被官方Maven仓库所采用，是Maven仓库的事实标准。\n* 自动构建工具：既然使用了Maven的依赖管理，那么配套工具按理也应但选用Maven。然而在微服务的开发中，版本依赖比传统系统更为复杂，Maven的xml文件会变得非常难以维护。Gradle在兼容Maven依赖管理的基础上，使用了更为简洁的DSL描述语言，且构建速度更快，插件更为丰富。因此，我们选用Gradle 4.X作为微服务的构建工具。\n* 效率脚本与工具：开篇介绍微服务优缺点时已经提到。微服务架构下，经常需要新增微服务。为了降低新增成本，我们一套代码层面的脚本或工具来提升效率。我们会介绍“微服务初始化模板”、“更新Thrift RPC接口”等工具，以提升微服务的开发效率。\n\n本书的主线是微服务的架构及开发。为了保证这一主题的的稳定和连惯性，我们将上述研发工具链的使用单独抽提出来，在[《研发工具链》](../toolchain/README.md)一章中一并介绍。\n\n[^1]: [Why is Git better than Subversion?](https://stackoverflow.com/questions/871/why-is-git-better-than-subversion)\n"
  },
  {
    "path": "legacy/devops/README.md",
    "content": "# 运维工具链\n\n如果你一直从事研发职位，或很少接触运维岗位，可能会觉得将\"运维\"放到架构设计中，有一些多余。\n\n事实上，上述想法大错特错，我们来看几个案例:\n1. \"刚才的上线，研发人员是从test分支打的jar包，引入了新的bug，赶快从主分支打个jar包，我要重新上线...\"\n1. \"在我的本地跑的很好的啊，一上到线上就NPE，这线上环境有毒!\"\n1. \"从一大早就开始上线，每次都要重新打jar包，拷贝、重启...郁闷的是有个顽固的bug，研发改了3次，我也上了3次，现在已经晚上9点了，还没上完...\"\n1. \"这个服务周日就挂掉了，直到周一早上才被发现，客服电话都被打爆了！\"\n\n如果你曾在一些不重视运维技术的公司呆过，一定对上述场景颇有感触。\n\n如果你恰好没有经历过上述场景，我推荐你读一下《凤凰项目:一个IT运维的传奇故事》。\n\n相信读完后，你会对\"运维部门\"有更加深刻的理解。\n\n运维并不是简单的\"将代码推上线\"，他至少应当包含两个职能:\n1. 尝试通过\"自动化\"、\"可重用\"、\"稳定\"的方式解决部署问题。即打造所谓的\"持续部署\"平台。\n1. 维护公司内的基础设施，例如后端服务所需要的数据库、消息队列等组建。\n1. 监控系统运行状况，尝试自动恢复故障，对潜在的故障作出预警。\n\n看了上述介绍后，你还会觉得在\"运维\"是\"架构设计\"中可有可无的一部分么?\n\n当然，本书并不是以运维作为核心主题的。因此，在本章中，我们将介绍运维工作与微服务架构关系最紧密的几个部分:\n1. 后端组建的运维，包括Docker仓库、数据库、缓存、消息队列\n1. 日志与监控，日志的收集、异常报警系统，平台监控系统。\n1. 生产、测试环境的构建，如跳板机、机房打通等。\n\n好了，让我们开启微服务架构下，运维工作的新篇章吧。\n"
  },
  {
    "path": "legacy/devops/discovery.md",
    "content": "# Nginx REST网关自动配置\n\n"
  },
  {
    "path": "legacy/devops/docker-repo.md",
    "content": "# Docker 私有仓库\n\n在前面的章节中，我们使用了Kubernetes和容器技术实现了微服务的发现、负载均衡、持续部署等需求。\n\n然而，我们并未提到Docker镜像的配置。默认的，我们使用了Docker官方默认的Docker镜像。\n\n然而在实际工作中，我们最好使用Docker私有仓库。\n\n想象一下，持续部署流程中，我们会将微服务的jar包自动构建，并打成Docker镜像，推送到Docker镜像服务器，然后部署到Kubernetes集群上。\n\n想象下，如果我们使用默认的公共镜像，等于将自己的产品完全\"开源\"地暴露给了互联网。这里\"开源\"打了引号，虽然打成jar包后都是class文件，但是可以通过反编译工具轻松的解析到源代码，和开源是差不多的。\n\n因此，与之前的[私有maven仓库](toolchain/nexus.md)类似，我们也需要一个私有的Docker仓库。\n\n## 启动私有仓库的Kubernetes服务\n\n有意思的是，Docker私有仓库(Docker registry)本身也是一个Docker镜像。有没有鸡生蛋，蛋生鸡的感觉:-)\n\n与之前所有的服务类似，我们也将Docker私有仓库部署在Kubernetes上。\n\n首先，还是先创建物理机上的挂载点:\n\n```shell\nsudo mkdir /data/registry/\n\nsudo chmod -R 777 /data/registry/\n```\n\n然后创建部署, lmsia-docker-registry.yaml:\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: lmsia-docker-registry-deployment\nspec:\n  selector:\n    matchLabels:\n      app: lmsia-docker-registry\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        app: lmsia-docker-registry\n    spec:\n      restartPolicy: Always\n      nodeSelector:\n        kubernetes.io/hostname: minikube\n      containers:\n      - name: lmsia-docker-registry-ct\n        image: registry:2.6.2\n        ports:\n        - containerPort: 5000\n          hostPort: 5000\n        volumeMounts:\n        - mountPath: \"/auth\"\n          name: volume\n          subPath: auth\n        - mountPath: \"/var/lib/registry\"\n          name: volume\n          subPath: registry\n        env:\n        - name: \"REGISTRY_STORAGE_DELETE_ENABLED\"\n          value: \"true\"\n      volumes:\n      - name: volume\n        hostPath:\n          path: /data/registry/\n```\n\n在上面的描述文件中，进行了如下配置：\n* 创建了registry容器2.6.2版本，暴露端口5000\n* 强制绑定到物理机\"minikube\"上，挂掉自动重启\n* 支持删除镜像\n\n应用一下，成功：\n```shell\nkubectl apply -f ./lmsia-docker-registry.yaml\n```\n\n## 向私有仓库发布镜像 \n\n在本书架构的应用场景下，私有仓库的使用场景是:\n* jenkins完成自动构建，并向私有仓库发布镜像\n* 其他Kubernetes节点，从私有仓库拉取镜像，启动Pod\n\n我们这里先完成第一步，我们登录minikube来模拟发布镜像:\n\n首先登录私有仓库\n```shell\nminikube ssh\n\n$docker login localhost:5000\n\ntest\npass\n\n```\n需要说明的是，我们创建的私有仓库，默认有一个用户test/pass，如果你认为安全性不够的话，可以参考[官方文档](https://docs.docker.com/registry/deploying)自行修改，这里不再赘述。\n\n还需要登录共有仓库\n```shell\n$docker login\n\ncoder4\nxxxxxx\n```\n注意:这里共有仓库的登录步骤不可少，因为我们接下来需要在本地读取共有仓库的镜像。\n\n然后我们编辑一个镜像Dockerfile:\n```shell\nFROM alpine\nCMD sleep 3600\n```\n\n编译并发布到私有仓库上\n```shell\n$docker build -t alpine_test .\n$docker tag alpine_test $DR_DOMAIN/alpine_test:test_1.0\n$docker push $DR_DOMAIN/alpine_test\n```\n\n至此，我们已经发布到了私有仓库上，查询后，发现成功了：\n```shell\n$ curl http://localhost:5000/v2/_catalog\n\n{\"repositories\":[\"alpine_test\"]}\n```\n\n## Kubernetes从私有仓库拉取镜像\n\n对于Kubernetes集群而言，我们不太可能登录到每台机器上手工执行docker login。\n\n幸运的是，Kubernetes为我们提供了解决方案。\n\n创建一个regcred，相当于一个在集群内部通用的凭证:\n```shell\nkubectl create secret docker-registry regcred --docker-server=192.168.99.100:5000 --docker-username=user --docker-password=pass --docker-email=lihy@coder4.com\n\nsecret \"regcred\" created\n```\n\n查看一下:\n```shell\nkubectl get secret regcred --output=\"jsonpath={.data.\\.dockerconfigjson}\" | base64 -d\n\n{\"auths\":{\"192.168.99.100:5000\":{\"username\":\"user\",\"password\":\"pass\",\"email\":\"lihy@coder4.com\",\"auth\":\"dXNlcjpwYXNz\"}}}\n```\n\n下面，我们来创建一个使用这个私有仓库的Pod,看一下yaml\n```yaml\napiVersion: v1\nkind: Pod\nmetadata:\n  name: lmsia-private-test\nspec:\n  containers:\n  - name: lmsia-private-test\n    image: 192.168.99.100:5000/alpine_test:test_1.0\n  imagePullSecrets:\n  - name: regcred\n\n```\n\n如上，特殊的配置有:\n* 我们在image定义之前，增加了私服的前缀\n* 最后增加了刚才配置好的imagePullSecrets\n\napply之后，可以发现启动成功了。\n```shell\nkubectl get pods\nNAME                                                READY     STATUS    RESTARTS   AGE\nlmsia-docker-registry-deployment-569fd8b594-ldch2   1/1       Running   0          2m\nlmsia-private-test                                  1/1       Running   0          57s\n```\n\n提醒一下，如果启动失败，并且错误原因是:\n```shell\n  Warning  Failed                 4s (x2 over 20s)  kubelet, minikube  Failed to pull image \"192.168.99.100:5000/alpine_test\": rpc error: code = Unknown desc = Error response from daemon: Get https://192.168.99.100:5000/v2/: http: server gave HTTP response to HTTPS client\n```\n\n那么，请参考这篇文章进行解决[insecure repository in minikube](https://github.com/kubernetes/minikube/blob/master/docs/insecure_registry.md)\n\n至此，我们完成了Docker私有仓库的搭建和访问。\n\n## 思考与拓展\n* 在Docker Registry 2后，默认强制采用加密认证方式，请结合[这篇文章](http://tech.paulcz.net/2016/01/deploying-a-secure-docker-registry/)，将私有仓库的部署改为加密方式。\n"
  },
  {
    "path": "legacy/devops/jump-server.md",
    "content": "# 线上跳板机\n\n"
  },
  {
    "path": "legacy/devops/openvpn-k8s.md",
    "content": "# OpenVPN访问Kubernetes集群内网\n\n搭建好的Kubernetes集群中，默认是存在网络隔离的，即集群内部使用一套独立的网络，与物理网络相互隔离。\n\n为了将内部服务暴露给外部调用，Kubernetes提供了ClusterIP等多种方式。\n\n但在有的时候，我们能够从本地网络直接访问Kubernetes集群内网的所有结点：\n* 本地调试微服务时，可能要调用许多微服务，而这些微服务所部署的Pod或Service并没有配置对外暴露的Cluster IP\n* 我们需要登录Pod查看内存、日志等信息\n\n实现这类功能，可以有两种方式：\n* 在本地网络和集群内网之间，配置路由协议，从而实现互联互通。\n* 通过VPN的方式，打通物理网络和集群内网。\n\n路由配置的方式看起来最直观，但有时难以实施:\n* 配置路由协议需要专业级的路由器、也需要学习路由的配置，设备采购成本、学习成本较大\n* 如果本地和机房之间需要通过互联网而不是局域网访问，那么路由协议的配置需要运营商配合，基本无法实现\n\n在本小节，我们主要介绍第二种方式，即通过VPN穿透的方式，从本地网络直接访问Kuberntes集群内网。\n\n在深入技术细节前，我们先来了解下VPN的原理，如下图所示：\n\n![VPN原理](./vpn.gif \"VPN原理\")\n\n如上图所示，VPN通过互联网链接，建立其一条本地网络到远程内网之间的隧道。远程内网的含义是：有一台具有公网IP的网关，但内网本身没有暴露给公网。当VPN连接建立后，就可以直接从本地网络访问远程内网中的所有资源。\n\n在本小节，我们选用OpenVPN，这是一个开源的VPN实现，选用理由有：\n* 客户端支持广泛：Windows, Linux, Mac, Android，iOS等几乎所有主流平台\n* 传输层协议同时支持TCP和UDP\n* 协议流行，在一些软路由(如DD-WRT)上都有直接实现，方便了日后拓展\n\n## OpenVPN路由服务的配置\n\n配置OpenVPN路由主要分为两大步骤：\n* 配置服务端\n* 客户端连接\n\n在配置OpenVPN服务前，我们再回顾一下这节的标题\"OpenVPN访问Kubernetes集群内网\"，我们要做的是访问Kubernetes集群内网。\n\n我们假设你已经架设了Kubernetes集群，并使用了calico网络模型。\n\n我们先来看一下服务端的配置，在启动服务前，先要生成Open VPN所需要的密钥:\n\n```shell\n#!/bin/bash\nVOLUME=\"$HOME/openvpn\"\nvpn_ip=\"vpn.coder4.com\"\n# init for first time only\nrm -rf $VOLUME\nmkdir -p $VOLUME \ndocker run -v $VOLUME:/etc/openvpn --rm kylemanna/openvpn ovpn_genconfig -u udp://$vpn_ip -s 10.4.0.0/24\ndocker run -v $VOLUME:/etc/openvpn --rm -it kylemanna/openvpn ovpn_initpki \n```\n\n如上所示，我们直接使用了封装好的[kylemanna/openvpn](https://github.com/kylemanna/docker-openvpn)这个Docker镜像。\n* 上述请在集群的任意一台物理机上执行，这台物理机之后会作为OpenVPN的接入点，所以一定要有公网IP\n* 生成配置到本地HOME目录的openvpn文件夹\n* vpn内网范围是10.4.0.0/24，这只得是VPN隧道所使用的网段，一定要注意，不要和已有物理网络、Kubernetes网络冲突。\n* 通信协议是UDP，我们假设这台物理机有DNS指向vpn.coder4.com\n\n生成配置后，就可以启动VPN服务器了:\n\n```shell\n#!/bin/bash\n \n# submit to tool node\nNAME=\"openvpn\"\nVOLUME=\"$HOME/openvpn\"\ndns_ip=\"10.96.0.10\"\n \n# stop & run server (should call init_open_vpn_test.sh before) \ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --name $NAME \\\n    --network bridge \\\n    --dns $dns_ip \\\n    -d \\\n    -v $VOLUME:/etc/openvpn \\\n    -p 1194:1194/udp \\\n    --cap-add=NET_ADMIN \\\n    --restart always \\\n    kylemanna/openvpn \\\n    ovpn_run --cipher AES-128-CBC\n```\n\n如上所述，我们启动了VPN服务器:\n* 注意我们这里是直接用的docker启动，而非放到Kubernetes集群中\n* 通过bridge即NAT的方式连接物理服务器，实际上在calico网络模型下，所有的Kubernetes集群内网IP都可以通过物理机访问，所以这里我们用了桥接的方式即可实现访问。\n* 绑定到物理机1194的UDP端口上\n* 加密算法用的AES-128-CBC\n\n经过上述配置后，我们可以在本地通过netcat访问，如果能成功连接而没有\"Connection Refused\"，就说明服务启动成功了。\n\n```shell\nnc -u vpn.coder4.com 1194\n```\n\n## 配置客户端\n\n在服务端启动后，我们需要能够从客户端真正的建立OpenVPN隧道。\n\nOpenVPN默认是支持多用户的（即可以有不同用户连接VPN），我们首先要为用户在OpenVPN系统中创建帐号, create_vpn_user.sh：\n\n```shell\n#!/bin/bash\n \nif [ x\"$#\" != x\"1\" ];then\n    echo \"Usage: $0 <username>\"\n    exit -1\nfi\n \nUSERNAME=\"$1\"\nOVPN_FILE=\"$USERNAME.ovpn\"\nCIPHER=\"AES-128-CBC\"\nDNS_IP=\"10.96.0.10\"\nROUTE_CMD=\"route 192.168.0.0 255.255.0.0\"\n \nVOLUME=\"$HOME/openvpn\"\n# generate client cert for username \ndocker run -v $VOLUME:/etc/openvpn --rm -it kylemanna/openvpn easyrsa build-client-full $USERNAME nopass\ndocker run -v $VOLUME:/etc/openvpn --rm kylemanna/openvpn ovpn_getclient $USERNAME > $OVPN_FILE\n \n# post process\nsed -i 's/redirect-gateway.*$//' $OVPN_FILE\n \ncat >> $OVPN_FILE <<EOF\n \n# disable lzo\ncomp-lzo no\n \n# add this line, the k8s network route\n$ROUTE_CMD\n \n# dns update\ndhcp-option DNS $DNS_IP \nscript-security 2\nup /etc/openvpn/update-resolv-conf\ndown /etc/openvpn/update-resolv-conf\n \n# security\ncipher $CIPHER \n \nEOF\n```\n\n如上执行\"create_vpn_user.sh coder4\"，则会创建一个名为coder4的用户，并生成一个\"coder4.ovpn\"文件到本地。\n\n我们看一下这个文件：\n```\nclient\nnobind\ndev tun\nremote-cert-tls server\n\ncomp-lzo no\n\nremote vpn.coder4.com 1194 udp\n\n<key>\n-----BEGIN PRIVATE KEY-----\nxxxx\n-----END PRIVATE KEY-----\n</key>\n<cert>\n-----BEGIN CERTIFICATE-----\nxxxx\n-----END CERTIFICATE-----\n</cert>\n<ca>\n-----BEGIN CERTIFICATE-----\nxxxx\n-----END CERTIFICATE-----\n</ca>\nkey-direction 1\n<tls-auth>\n#\n# 2048 bit OpenVPN static key\n#\n-----BEGIN OpenVPN Static key V1-----\nxxxx\n-----END OpenVPN Static key V1-----\n</tls-auth>\n\n\n\n# add this line, the k8s network route\nroute 192.168.0.0 255.255.0.0\n\n# dns update\n#dhcp-option DNS 10.96.0.10 \nscript-security 2\nup /etc/openvpn/update-resolv-conf\ndown /etc/openvpn/update-resolv-conf\n\n# security\ncipher AES-128-CBC \n\n\n```\n\n如上所述：\n* 证书部分已经被省略\n* 禁用lzo压缩\n* 远程服务器地址是vpn.code4.com，协议是udp \n* vpn建立连接后，自动设置路由192.168.0.0/16，这个是Kubernets集群内网的路由\n* 应用dns服务器10.96.0.10，这个也是Kubernetes集群默认的\n\n生成文件后，我们在本地网络建立vpn连接:\n```shell\nopenvpn ./coder4.ovpn\n```\n\nvpn链接建立成功后，我们尝试ping一个Kubernetes集群内的地址(假设集群内已经启动了若干容器):\n```shell\nping 192.168.1.2\n\nPING  (192.168.1.2) 56(84) bytes of data.\n64 bytes from 192.168.1.2: icmp_seq=1 ttl=48 time=8.39 ms\n64 bytes from 192.168.1.2: icmp_seq=2 ttl=48 time=8.34 ms\n64 bytes from 192.168.1.2: icmp_seq=3 ttl=48 time=8.41 ms\n\n```\n\n访问成功！至此，我们通过OpenVPN的方式，成功打通了本地网络和远程Kubernetes集群的内网。\n\n最后，还需要再说明两点:\n1. Kubernetes网络模型很多，实现差别很大，本文所述的k8s + calico + docker bridge(nat)的方式 配合才能生效，其他网络模型和组合不保证能成功\n1. 由于一些你懂的原因，如果openvpn的server和client之间的互联网跨国了，会被断开或者无法访问，UDP也不行，不过对于微服务的应用场景，影响不大，一般都是服务器部署在国内，开发人员也在国内。\n\n## 拓展与思考\n1. 在k8s中，POD与Service IP一般不会分配在同一网段中。如果我们的客户端也想访问Service IP，应当在哪一步、增加哪些配置？\n1. 本节通过OpenvVPN实现了从本地（单机）访问Kubernetes集群内网。如果想让本地局域网内，所有机器都可以访问远程Kubernetes集群的内网，应当如何配置呢？ \n"
  },
  {
    "path": "legacy/k8s/README.md",
    "content": ""
  },
  {
    "path": "legacy/k8s/docker-k8s.md",
    "content": "# 集装箱、容器化、容器编排\n\n## 集装箱革命、容器化革命\n\n前面已经提到，微服务架构离不开容器技术。为什么需要容器呢？\n\n我们先来看一个海运的例子。\n\n* 传统海运：尽管货海运已经出现了几千年，但直到20世纪中叶，货物运输依然是一种劳动力密集型工作。码头雇佣了数以万计的工人，将货物从岸上搬运到船舱中。由于货物的种类繁多，体积不一、传送带、铲车都不能根本的解决问题，货物装卸依然大量依赖人工，而且装卸时间大量占用了港口时间，装卸价格居高不下。\n* 集装箱革命：20世纪50年代开始，集装箱逐渐走向商用的舞台。货物在岸上按照整齐的规格码放整齐，从而可以封装进集装箱。而装卸货物只需要机械来搬运集装箱即可，极大提高了港口的装卸效率。\n\n集装箱革命使得货物的装卸成本从5美元/吨骤降到16美分/吨，节约了97%...\n\n容器技术的兴起，也成为了运维领域的另一场\"集装箱式革命\"：\n\n- 容器就是集装箱\n- 货物则是运行与容器中的、各式应用程序。\n\n容器为运维工作带来了如下革命性进展：\n\n- 容器的标准化：为了让应用程序在生产机上跑起来，经常需要做各种不同操作系统的安装，依赖软件库的安装、环境配置......这些过程非常繁琐，还经常会由于版本的细微差异，和开发环境不一致，出现“这个程序本地好好的，放到服务器上就崩溃”这类情况。容器可以使用统一的描述语言，快速构建出完全相同的、标准化环境，从而解决运行环境的问题。\n- 容器的隔离化：此外，不同的应用程序需要不同的应用环境，如果都部署在一台物理机上，很可能会发生包、依赖冲突，导致无法运维，而容器可以为运行在同一台物理机上的应用程序，创建不同的隔离环境。\n\n不仅在运维领域，容器技术在开发环境的搭建、软件教学等领域，也产生了深远的影响。如果你使用过Docker，相信一定对容器带来的变革深有感触，这里就不再一一列举:-)\n\n## 容器、容器编排\n\n如果你应用过容器技术，那么一定听说过该领域内最迟手可热的两个技术：Docker和Kubernetes。\n\n同样都是容器化技术的代表，这两者有什么不同呢？\n\n我们来看一下基本定义：\n\n- Docker：一种容器化引擎\n- Kubernetes：一种容器服务的编排系统\n\nDocker的定义比较好理解，它是一种容器的运行时环境。此外，Docker还提供了Dockerfile，这是一种运行环境描述语言。我们可以借助它来快速构建出应用程序所需的、可移植的环境。\n\nKubernetes的“容器编排”定位，就不是很好理解了，我们先来看一些应用容器技术时遇到的实际问题：\n\n- 如何部署一个应用的多个结点，并进行自动的负载均衡？\n- 应用不是孤立的，容器的运行也会有依赖关系，如何进行管理？\n- 如何在不影响业务运行的前提下，升级容器（内的应用）？\n- 如何在容器应用挂掉的时候，自动恢复故障？\n\n上述问题，已经超越了容器引擎的管理范畴，这些就是“容器编排”系统所要考虑的事情了。\n\n从上述例子我们不难看出，将Docker直接与Kubernetes来进行比较，并不科学，因为他们不是同一个范畴的技术。\n\n实际上，容器引擎有很多种，除了最流行的Docker外，还有[rkt](https://coreos.com/rkt/docs/latest/)、[LXC](https://linuxcontainers.org/)等。从容器编排引擎的角度来看，无论是Docker、rkt还是LXC，都只是运行时引擎，都是可以相互替换的。\n\n类似地，容器编排引擎也有很多种，除了最流行的Kubernetes外，还有[Swarm](https://docs.docker.com/engine/swarm/)、[MESOS](http://mesos.apache.org/)。只不过后两者的发展并不顺利，Kubernetes已经获得了容器编排领域的霸主地位。\n\n前面谈了Docker与Kubernetes的许多不同，但他们也有一些共同点：\n\n- 都是基于容器技术\n- 都是采用GO语言编写的\n- 都是用yaml作为配置文件格式\n- 都是开源社区项目\n\n以Docker为代表的容器技术、以Kubernetes为代表的容器编排技术，也对微服务的发展也起到了推动作用。\n\n试想一下如果你将一个单体服务拆分为10个微服务，部署、运维成本将陡然上升。如何解决这些问题呢？这恰好是容器、容器编排的用武之地。\n\n从容器一下子跳到微服务，你可能还看的有些懵，不要紧，我会在本章接下来的章节为你逐步展开。\n\n## 思考与拓展\n\n1. 如果你想了解更多关于容器、容器编排的事情，可以参考容器周刊上的一篇文章[《Kubernetes vs. Docker: A Primer》](https://containerjournal.com/topics/container-ecosystems/kubernetes-vs-docker-a-primer/)\n2. 不同的容器引擎Docker、rkt等，各有什么优劣么？\n3. Swarm与Docker出自同一公司。为什么Docker引擎成功了，Swarm编排引擎却被Kubernetes打败了呢？\n\n\n\n\n\n\n\n"
  },
  {
    "path": "legacy/k8s/helm.md",
    "content": "# 使用Helm进行包管理\n\n通过前面的章节，我们已经学会了如何在Kubernetes上启动Pod、Deployment及Service。\n\n我们再来简单回顾一下流程过程（以Service为例）：\n\n编写yaml文件，其中要包含如下信息：\n\n1. Pod信息\n2. Deployment信息\n3. Service（ClusterIP）信息\n\n然后通过kubectl应用。\n\n如果你多部署几个Service，就会发现编写yaml是一个非常繁琐的过程，有没有方法可以简化这一操作呢？\n\n答案当然是肯定的，我们可以引入包管理工具Helm。\n\nHelm与Kubernetes的关系，类似于apt与Ubuntu的关系：你当然可以通过编译的方法，手动安装软件；但通过apt install安装软件更加简单方便。\n\n我们先了解下Helm中的几个基本概念：\n\n- Chart：Helm中的软件包，类似于Ubuntu中的deb\n- Release：Chart的部署实例，在同一个Kubernetes集群中，同一个Chart可以部署多份。\n- Repository：Chart的仓库，可以支持镜像。\n\n下面，跟着我，一起体验Helm的强大之处吧：\n\n## 安装Helm、设置镜像\n\nHelm 3已经正式发布了，最显著的改进是：移除了被人诟病的Tiller，我们以3为例进行讲解。\n\n如无特殊说明，本文的所有操作都在Kubernets的master节点上执行。\n\n首先下载二进制包：\n\n```\nwget https://get.helm.sh/helm-v3.0.1-linux-amd64.tar.gz\ntar -xzvf helm-v3.0.1-linux-amd64.tar.gz\n```\n\n解压缩后，得到helm文件，版本是3.0.1\n\n```bash\n./helm version\nversion.BuildInfo{Version:\"v3.0.1\", GitCommit:\"7c22ef9ce89e0ebeb7125ba2ebf7d421f3e82ffa\", GitTreeState:\"clean\", GoVersion:\"go1.13.4\"}\n```\n\n由于众所周知的原因，我们切换到国内源：\n\n```bash\nhelm repo add stable http://mirror.azk8s.cn/kubernetes/charts/\n```\n\n添加后验证一下：\n\n```bash\n./helm repo list\nNAME  \tURL\nstable\thttp://mirror.azk8s.cn/kubernetes/charts/\n```\n\n至此，我们已经完成了helm的配置。\n\n## 使用Helm部署Memcached集群\n\n由于本书还没有涉及Kubernetes的存储部分，我们无需存储的Memcached为例，讲解如何使用helm完成快速部署。\n\n首先搜一下Charts：\n\n```bash\n./helm search repo memcached\nNAME            \tCHART VERSION\tAPP VERSION\tDESCRIPTION\nstable/memcached\t3.2.1        \t1.5.20     \tFree & open source, high-performance, distribut...\nstable/mcrouter \t1.0.2        \t0.36.0     \tMcrouter is a memcached protocol router for sca...\n```\n\n我们选择stable的memcached进行部署：\n\n```bash\n./helm install test-memcached stable/memcached\n```\n\n其中test-memcached是service的名字，默认会部署一个3节点的，有状态的memcached集群\n\n部署成功后输出如下：\n\n```bash\nNAME: test-memcached\nLAST DEPLOYED: Thu Dec 12 17:46:36 2019\nNAMESPACE: default\nSTATUS: deployed\nREVISION: 1\nTEST SUITE: None\nNOTES:\nMemcached can be accessed via port 11211 on the following DNS name from within your cluster:\ntest-memcached.default.svc.cluster.local\n\nIf you'd like to test your instance, forward the port locally:\n\n  export POD_NAME=$(kubectl get pods --namespace default -l \"app=test-memcached\" -o jsonpath=\"{.items[0].metadata.name}\")\n  kubectl port-forward $POD_NAME 11211\n\nIn another tab, attempt to set a key:\n\n  $ echo -e 'set mykey 0 60 5\\r\\nhello\\r' | nc localhost 11211\n\nYou should see:\n\n  STORED\n```\n\n稍等一会，可以看一下Pod状态：\n\n```bash\nkubectl get pods\ntest-memcached-0            1/1     Running            0          25m\ntest-memcached-1            1/1     Running            0          25m\ntest-memcached-2            0/1     Pending            0          24m\n```\n\n从上图可以发现，2个节点已经启动成功，第3个在Pending，这是因为我的集群中只有2台机器，并且memcached的Charts中规定了每台node上只能启动一台memcached。\n\n如果你直接ping域名“test-memcached.default.svc.k8s.coder4.com”的话，会发现不通，这是因为在master节点上，并没有使用k8s集群内的dns服务器。\n\n我们启动一个临时pod(tiny-tools)来验证一下域名：\n\n```bash\nkubectl run -i --tty tiny-tools --image=giantswarm/tiny-tools --restart=Never -- sh\n$ ping test-memcached.default.svc.k8s.coder4.com\nPING test-memcached.default.svc.k8s.coder4.com (10.36.0.1): 56 data bytes\n64 bytes from 10.36.0.1: seq=0 ttl=64 time=0.081 ms\n64 bytes from 10.36.0.1: seq=1 ttl=64 time=0.061 ms\n64 bytes from 10.36.0.1: seq=2 ttl=64 time=0.061 ms\n.....\n```\n\n在tiny-tools中，可以ping成功，我们再深入验证下域名解析：\n\n```bash\n$ dig test-memcached.default.svc.k8s.coder4.com\n\n; <<>> DiG 9.14.3 <<>> test-memcached.default.svc.k8s.coder4.com\n;; global options: +cmd\n;; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 18530\n;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1\n;; WARNING: recursion requested but not available\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 4096\n; COOKIE: 02680bbf7152f858 (echoed)\n;; QUESTION SECTION:\n;test-memcached.default.svc.k8s.coder4.com. IN A\n\n;; ANSWER SECTION:\ntest-memcached.default.svc.k8s.coder4.com. 30 IN A 10.36.0.1\ntest-memcached.default.svc.k8s.coder4.com. 30 IN A 10.44.0.1\n\n;; Query time: 1 msec\n;; SERVER: 10.100.0.10#53(10.100.0.10)\n;; WHEN: Thu Dec 12 10:26:21 UTC 2019\n;; MSG SIZE  rcvd: 196\n```\n\n这个域名解析到了两个POD上：10.36.0.1、10.44.0.1，与前面的启动符合。\n\n如果我们在集群内，想通过域名访问某一台memcached，也是可以的：\n\n```bash\n$ ping test-memcached-0.test-memcached\nPING test-memcached-0.test-memcached (10.44.0.1): 56 data bytes\n64 bytes from 10.44.0.1: seq=0 ttl=64 time=0.053 ms\n64 bytes from 10.44.0.1: seq=1 ttl=64 time=0.063 ms\n64 bytes from 10.44.0.1: seq=2 ttl=64 time=0.063 ms\n.....\n```\n\n在Kubernetes集群外（比如master节点上），我们暂时只能通过IP来进行访问：\n\n```\nping 10.44.0.1\nPING 10.44.0.1: 56 data bytes\n64 bytes from 10.44.0.1: seq=0 ttl=64 time=0.053 ms\n64 bytes from 10.44.0.1: seq=1 ttl=64 time=0.063 ms\n64 bytes from 10.44.0.1: seq=2 ttl=64 time=0.063 ms\n```\n\n至此，借助Helm，我们只需一行命令，就完成了memcached集群的部署。\n\n## 拓展与思考\n\n1. 如何在Kubernetes集群外，通过域名进行访问呢？\n1. Helm可以部署有复杂依赖关系的多组服务么？\n\n\n\n\n\n\n\n"
  },
  {
    "path": "legacy/k8s/k8s-cluster.md",
    "content": "# 搭建Kubernetes集群\n\nminikube是入门Kubernetes的优秀工具。使用minikube，可以轻松地在本地运行Kuberntes的主要功能。\n\n为了演示方便，本书如有涉及到Kubernetes的章节，多数都是在minikube上运行的。\n\n然而，minikube并不是为生产环境而设计的，当产品真正上线后，是需要部署到真正的Kubernetes集群中的。\n\n在本节中，我们将探讨如何部署真正的Kuberntes集群。\n\n## 环境准备\n\n正所谓“三人成群”，一般来说\"集群\"需要至少有3台机器。为了方便演示，我们也搭建一台具有3个物理机的Kuberntes集群，1台master，2台slave。\n\n在搭建之前，需要做如下准备:\n* 3台物理机安装了Ubuntu 18.04系统[^1]\n* 3台物理机具有内网IP，假设为192.168.8.165 ~ 192.168.8.167\n* 3台物理设置主机名为k1 ~ k3，通过hostname可以内网互通\n* 3台物理机需要联网，但不需要有公网IP\n\n### Docker环境安装\n\n准备好上述条件后，我们在3台机器上分别安装Docker：\n\n```shell\nsudo apt-get update && apt-get install -y apt-transport-https\nsudo apt install -y docker.io\nsudo systemctl start docker\nsudo systemctl enable docker\n```\n\n如上所述，安装成功后，我们手动启动了docker，并将其加入开机自启动中。\n\n接下来，我们将docker镜像切换到国内源\n\n```bash\nsudo vim /etc/docker/daemon.json\n```\n\n添加如下内容\n\n```bash\n{\n    \"registry-mirrors\": [\"https://registry.docker-cn.com\"]\n}\n```\n\n并记得重启\n\n```bash\nsystemctl restart docker\n```\n\n如果你既不是使用root、也没有使用docker用户执行安装，默认是缺少一些权限的，我们添加一下：\n\n```\n# 将自己添加到docker组中\nsudo groupadd docker\nsudo gpasswd -a ${USER} docker\n# 重启后重新load下权限\nsudo service docker restart\nnewgrp - docker\n```\n\n再强调一次，3台机器上都要执行上述同样的操作。\n\n至此，我们已经准备好了Docker环境。\n\n### Kubernetes的二进制安装\n\n有了Docker后，我们来安装Kubernetes。\n\n由于众所周知的原因，直接使用谷歌的镜像，是没法进行安装的，我们该用aliyun的镜像。\n\n```\nsudo /etc/apt/source/apt.list\n```\n\n尾部添加如下内容\n\n```\ndeb http://mirrors.aliyun.com/kubernetes/apt kubernetes-xenial main\n```\n\n然后更新源\n\n```\nsudo apt-get update\n```\n\n报错了\n\n```\nW: GPG error: http://mirrors.aliyun.com/kubernetes/apt kubernetes-xenial InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 6A030B21BA07F4FB\n```\n\n不要慌张，我们只需要修复下PGP，先复制下出错PGP的后6位，然后执行：\n\n```\n gpg --keyserver keyserver.ubuntu.com --recv-keys BA07F4FB\n```\n\n然后导入并添加，这次是完整PGP了：\n\n```\ngpg -a --export 6A030B21BA07F4FB | sudo apt-key add -\n```\n\n之后再更新&安装，应该就不会有问题了\n\n```\nsudo apt-get install -y kubelet kubeadm kubectl kubernetes-cni\n```\n\n安装后，我们看一下当前版本，目前是1.16.3，这个版本号后面还会用到。\n\n```\nkubelet --version Kubernetes v1.16.3\n```\n\n安装完毕后，我们需要关闭一下swap\n\n```\nsudo swapoff -a\n```\n\n注意，上述关闭swap的方法，只针对本次启动有效。\n\n如果想永久关闭swap，需要进行2步操作：\n\n```\nsudo vim /etc/fstab\n禁用swapfile开头的那一行\n```\n\n接着\n\n```\nsudo vim /etc/sysctl.conf\n# 添加如下行，保存重启\nvm.swappiness = 0\n```\n\n保存、重启后，就可以了。\n\n记得在3台机器上，都执行上述所有的操作。\n\n### Kubernetes的依赖镜像的准备\n\n有了二进制文件后，还需要依赖的镜像才可以部署集群，这些镜像都在gcr.io上。\n\n由于众所周知的原因，kubernetes启动集群时执行镜像下载会失败，我们需要将镜像提前安装好。\n\n首先将镜像下载到本地，这里我们使用Azure提供的国内镜像（给世纪互联点赞！）\n\n我们首先输入这条awk命令，会输出如下的docker开头的命令，然后执行这些命令：\n\n```\nkubeadm config images list --kubernetes-version v1.16.3 | awk -F \"/\" '{print \"docker pull gcr.azk8s.cn/google_containers/\"$2\"\"}'\n\ndocker pull gcr.azk8s.cn/google_containers/kube-apiserver:v1.16.3\ndocker pull gcr.azk8s.cn/google_containers/kube-controller-manager:v1.16.3\ndocker pull gcr.azk8s.cn/google_containers/kube-scheduler:v1.16.3\ndocker pull gcr.azk8s.cn/google_containers/kube-proxy:v1.16.3\ndocker pull gcr.azk8s.cn/google_containers/pause:3.1\ndocker pull gcr.azk8s.cn/google_containers/etcd:3.3.15-0\ndocker pull gcr.azk8s.cn/google_containers/coredns:1.6.2\n```\n\n镜像到本地了，但都是tag的模式，我们需要进行重命名，才能使用，同样的，执行docker开头的输出命令：\n\n```\nkubeadm config images list --kubernetes-version v1.16.3 | awk -F \"/\" '{print \"docker tag gcr.azk8s.cn/google_containers/\"$2\" k8s.gcr.io/\"$2\"\"}'\n\ndocker tag gcr.azk8s.cn/google_containers/kube-apiserver:v1.16.3 k8s.gcr.io/kube-apiserver:v1.16.3\ndocker tag gcr.azk8s.cn/google_containers/kube-controller-manager:v1.16.3 k8s.gcr.io/kube-controller-manager:v1.16.3\ndocker tag gcr.azk8s.cn/google_containers/kube-scheduler:v1.16.3 k8s.gcr.io/kube-scheduler:v1.16.3\ndocker tag gcr.azk8s.cn/google_containers/kube-proxy:v1.16.3 k8s.gcr.io/kube-proxy:v1.16.3\ndocker tag gcr.azk8s.cn/google_containers/pause:3.1 k8s.gcr.io/pause:3.1\ndocker tag gcr.azk8s.cn/google_containers/etcd:3.3.15-0 k8s.gcr.io/etcd:3.3.15-0\ndocker tag gcr.azk8s.cn/google_containers/coredns:1.6.2 k8s.gcr.io/coredns:1.6.2\n```\n\n至此，镜像已经可以直接使用了，但我们可以删除被tag的镜像，节省一些空间：\n\n```\nkubeadm config images list --kubernetes-version v1.16.3 | awk -F \"/\" '{print \"docker rmi gcr.azk8s.cn/google_containers/\"$2\"\"}'\n\ndocker rmi gcr.azk8s.cn/google_containers/kube-apiserver:v1.16.3\ndocker rmi gcr.azk8s.cn/google_containers/kube-controller-manager:v1.16.3\ndocker rmi gcr.azk8s.cn/google_containers/kube-scheduler:v1.16.3\ndocker rmi gcr.azk8s.cn/google_containers/kube-proxy:v1.16.3\ndocker rmi gcr.azk8s.cn/google_containers/pause:3.1\ndocker rmi gcr.azk8s.cn/google_containers/etcd:3.3.15-0\ndocker rmi gcr.azk8s.cn/google_containers/coredns:1.6.2\n```\n\n同样地，三台机器都要执行上述操作。\n\n至此，我们已经安装好了部署Kubernetes所需的软件、镜像。\n\n## 集群初始化\n\n在初始化Kuberntes集群前，我们先来解释一些重要参数:\n* API Advertise Address：这是Kubernetes Master对外提供交互和操作的接口，一般用内网IP地址\n* Pod Network CIDR: 在Kuberntes上运行的所有Pod都需要分配IP地址，会从这个CIDR池中分配。\n* Service CIDR: 类似Pod Network，Service分配的IP地址，会从这个CIDR池中分配\n* Service DNS Domain: 集群内网的后缀，默认是cluster.local\n\n了解了参数后，我们来初始化集群，在k1上执行:\n```shell\nsudo kubeadm init --kubernetes-version v1.16.3 --apiserver-advertise-address=192.168.8.165 --service-cidr=10.96.0.0/12 --pod-network-cidr=10.200.0.0/16 --service-dns-domain=cluter.coder4\n```\n\n如上所述:\n* 我们选用k1即192.168.8.165这台机器作为master\n* pod的cidr是10.200.0.0/16\n* service的cidr是10.96.0.0/12 (其实这也是k8s的默认值)\n* k8s内网的域名后缀是cluster.coder4\n\n执行过程可能稍长，最后结果大致如下：\n```shell\n...\nYour Kubernetes control-plane has initialized successfully!\n\nTo start using your cluster, you need to run the following as a regular user:\n\n  mkdir -p $HOME/.kube\n  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config\n  sudo chown $(id -u):$(id -g) $HOME/.kube/config\n\nYou should now deploy a pod network to the cluster.\nRun \"kubectl apply -f [podnetwork].yaml\" with one of the options listed at:\n  https://kubernetes.io/docs/concepts/cluster-administration/addons/\n\nThen you can join any number of worker nodes by running the following on each as root:\n\nkubeadm join 192.168.8.165:6443 --token lcyhu1.ie6owlcotmrwiydv \\\n    --discovery-token-ca-cert-hash sha256:4c345192970992063a0e704ef03ef831a48a368c686047bc32871334292ce091\n```\n\n前面打了...的地方，表示还有很长的输出，我们可以不用关心过程，只看最后这些结果。\n\n首先我们按照提示执行配置命令：\n\n```bash\nmkdir -p $HOME/.kube\nsudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config\nsudo chown $(id -u):$(id -g) $HOME/.kube/config\n```\n\n接着，我们部署网络组件，这里我们选用calico的最新版3.9\n\n```bash\nsysctl net.bridge.bridge-nf-call-iptables=1 -w\nwget https://docs.projectcalico.org/v3.9/manifests/calico.yaml\nsed -i -e \"s?192.168.0.0/16?10.200.0.0/16?g\" calico.yaml\nkubectl apply -f ./calico.yaml\n```\n\n注意在上面，我们将calico的cird替换成了我们要使用的10.200.0.0/16，这一步不要忘记。\n\n都执行完毕后，我们查看下集群状态：\n\n```bash\nkubectl get nodes\n```\n\n应该能输出当前master的状态：\n\n```bash\nNAME   STATUS     ROLES    AGE   VERSION\nk1     NotReady   master   17s   v1.16.3\n```\n\n这里的NotReady是因为还没有Slave加入。\n\n你可能还注意到了最后有一行\"join ... token\"，先复制下来，后面会用到。\n\n## 加入Slave机器\n\n初始化好集群后，加入Slave机器的工作就非常简单了，还记得前面初始化时生成的token命令行么，我们直接拷贝过来，在k2和k3上执行：\n```shell\nsudo kubeadm join 192.168.8.165:6443 --token lcyhu1.ie6owlcotmrwiydv \\\n    --discovery-token-ca-cert-hash sha256:4c345192970992063a0e704ef03ef831a48a368c686047bc32871334292ce091\n```\n\n加入成功后，我们回到Master机器即k8s1上查看集群状态:\n```shell\nkubectl get nodes\n```\n\n```bash\nNAME   STATUS   ROLES    AGE     VERSION\nk1     Ready    master   3m37s   v1.16.3\nk2     Ready    <none>   105s    v1.16.3\nk3     Ready    <none>   91s     v1.16.3\n```\n\n可以看到，集群具有3台机器，1个master、2个slave，部署成功！\n\n## 集群重置\n\n有的时候，我们可能执行了错误的命令，或者想直接重建集群。\n\n此时，可以按照如下命令重置集群：\n\n在集群的每台机器上分别执行：\n\n```bash\nsudo ipvsadm --clear\nsudo kubeadm reset\nrm -rf /home/coder4/.kube/\n```\n\n[^1]: 这里使用的发行版为Ubuntu最新的LTS版本18.04。其他Linux发型版也是可以，不过后续的安装、配置命令会有略微不同，这里不再赘述。\n\n## 拓展与思考\n\n1. 前面提到了Kubernetes 提供了多种网络模型，他们的区别是什么，各自适用什么业务场景呢？请自行查找资料并回答。\n1. 在使用minikube部署Pod时，有时需要创建Volume，我们都是直接创建的本地PV。在正式的Kubernetes集群下，创建Volume有什么不同么？请自行查找资料并回答。\n"
  },
  {
    "path": "legacy/k8s/k8s-ha.md",
    "content": "# Kubernetes集群的高可用方案\n\n高可用(High Availability)是指系统可以“无中断”地提供服务的能力。\n\nKubernetes作为容器调度和编排的“操作系统”，高可用显得尤为重要。\n\n在之前的章节，我们搭建的都是“单主集群”，假设master节点挂掉，Kubernetes集群内容器的调度会受到影响\n\n针对“单主集群”、“master节点挂掉”这种情况，Kubernetes已经做了一些处理：已启动的Pod还将继续运行，但挂掉的、新增的Pod将无法被调度。\n\n可见，对于一个要求高可用的线上环境而言，上述的策略是远远不够的。\n\n本节，我们将讨论两种Kubernetes集群的高可用（HA）方案。\n\n## Kubernetes集群的HA方案\n\n构建Kubernetes集群的HA方案有很多种，我们首先介绍如何通过KeepAlived + HAProxy完成HA方案。\n\n我们假设在独立部署的网络环境（非共有云）中有如下的6台主机：\n\n- k1 192.168.8.191 (master)\n- k2 192.168.8.187 (master)\n- k3 192.168.8.186 (master)\n- k4 192.168.8.188\n- k5 192.168.8.190\n- k6 192.168.8.189\n- vip 192.168.8.10\n\n如上所述，k1~k3是主节点，k4~k6是普通节点，而vip(virtual ip)使用ip: 192.168.8.10。\n\n温馨提示，不要在共有云(阿里云、AWS)上尝试本小节的方案。\n\n### 高可用和负载均衡配置\n\n我们首先在主节点安装keepalived：\n\n```bash\nsudo apt-get install -y keepalived\n```\n\n然后对k1进行配置：\n\n```bash\n#sudo nano /etc/keepalived/keepalived.conf\n! Configuration File for keepalived\n\nglobal_defs {\n   router_id LVS_DEVEL\n}\n\nvrrp_script check_haproxy {\n    script \"killall -0 haproxy\" # check process alive\n    interval 3\n    weight -2\n    fall 10\n    rise 2\n}\n\nvrrp_instance VI_1 {\n    state MASTER          # backup -> BACKUP\n    interface eth0        # eth\n    virtual_router_id 51\n    priority 250          # < 250 for backup server\n    advert_int 1\n    authentication {\n        auth_type PASS\n        auth_pass 123456  # please change it for online\n    }\n    virtual_ipaddress {\n        192.168.8.10      # virtual ip not conflict with other\n    }\n    track_script {\n        check_haproxy\n    }\n}\n```\n\n如上所述，我们将k1的keepalived配置了默认master，并将vip绑定到eth0端口上。\n\n对于k2和k3，也要进行类似的配置，我们只需要将\"state MASTER\"修改为\"state slave\"即可。\n\n三台机器配置完成后，记得启动服务并设为开机自启动：\n\n```bash\nsudo service keepalived start\nsudo systemctl enable keepalived\n```\n\n我们在k1上查看效果：\n\n```bash\nip address show eth0\n2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000\n    link/ether 00:16:3e:0a:b3:b4 brd ff:ff:ff:ff:ff:ff\n    inet 192.168.8.191/24 brd 192.168.8.255 scope global dynamic eth0\n       valid_lft 315359417sec preferred_lft 315359417sec\n    inet 192.168.8.10/32 scope global eth0\n       valid_lft forever preferred_lft forever\n```\n\n如上所述，k1上的vip已经自动绑定到了eth0上，当k1挂掉后，k2和k3会竞争master，胜者获得vip。\n\n关于为什么要配置vip，我们稍后会详细讲述。\n\n接着我们安装haproxy：\n\n```bash\nsudo apt-get install -y haproxy\n```\n\nk1上进行配置：\n\n```bash\nsudo nano /etc/haproxy/haproxy.cfg\n#---------------------------------------------------------------------\n# Global settings\n#---------------------------------------------------------------------\nglobal\n    # to have these messages end up in /var/log/haproxy.log you will\n    # need to:\n    #\n    # 1) configure syslog to accept network log events.  This is done\n    #    by adding the '-r' option to the SYSLOGD_OPTIONS in\n    #    /etc/sysconfig/syslog\n    #\n    # 2) configure local2 events to go to the /var/log/haproxy.log\n    #   file. A line like the following can be added to\n    #   /etc/sysconfig/syslog\n    #\n    #    local2.*                       /var/log/haproxy.log\n    #\n    log         127.0.0.1 local2\n\n    chroot      /var/lib/haproxy\n    pidfile     /var/run/haproxy.pid\n    maxconn     4000\n    user        haproxy\n    group       haproxy\n    daemon\n\n    # turn on stats unix socket\n    stats socket /var/lib/haproxy/stats\n\n#---------------------------------------------------------------------\n# common defaults that all the 'listen' and 'backend' sections will\n# use if not designated in their block\n#---------------------------------------------------------------------\ndefaults\n    mode                    http\n    log                     global\n    option                  httplog\n    option                  dontlognull\n    option http-server-close\n    option forwardfor       except 127.0.0.0/8\n    option                  redispatch\n    retries                 3\n    timeout http-request    10s\n    timeout queue           1m\n    timeout connect         10s\n    timeout client          1m\n    timeout server          1m\n    timeout http-keep-alive 10s\n    timeout check           10s\n    maxconn                 3000\n\n#---------------------------------------------------------------------\n# kubernetes apiserver frontend which proxys to the backends\n#---------------------------------------------------------------------\nfrontend kubernetes\n    mode                 tcp\n    bind                 *:16443\n    option               tcplog\n    default_backend      kubernetes-apiserver\n\n#---------------------------------------------------------------------\n# round robin balancing between the various backends\n#---------------------------------------------------------------------\nbackend kubernetes-apiserver\n    mode        tcp\n    balance     roundrobin\n    server  k1 192.168.8.191:6443 check\n    server  k2 192.168.8.187:6443 check\n    server  k3 192.168.8.186:6443 check\n\n#---------------------------------------------------------------------\n# collection haproxy statistics message\n#---------------------------------------------------------------------\nlisten stats\n    bind                 *:1080\n    stats auth           admin:awesomePassword\n    stats refresh        5s\n    stats realm          HAProxy\\ Statistics\n    stats uri            /admin?stats\n\n```\n\n上述配置比较长，但关键点是将后台的6443端口映射到了haproxy的16443端口上。\n\n经过映射，我们访问haproxy的16443，将会自动负载均衡到k1~k3的某一台的6443端口上。\n\n我们将k2~k3执行同样的配置。\n\n配置完成后，记得启用并设置为开机启动。\n\n```bash\nsudo service haproxy restart\nsudo systemctl enable haproxy\n```\n\n你可能已经发现了，在haproxy的配置中，我们并没有绑定到vip的ip上，而是选择了bind *(绑定全部网络接口)。\n\n虽然k1~k3机器都有16443端口的负载均衡，但如果你想在集群中通过一个固定ip来访问api server，只能使用vip这个固定ip，这也就是为什么要选用vip的原因。\n\n接下来，我们将vip以\"host域名\"的方式，配置到k1~k6机器的host上：\n\n```bash\ncat >> /etc/hosts << EOF\n192.168.8.10 k8s-apiserver-vip\nEOF\n```\n\n至此，所以准备工作已经就绪。\n\n### 多主Kubernetes集群配置\n\n我们在k1上开始Kubernetes集群的配置\n\n```bash\n# nano cluster-config.yaml\napiVersion: kubeadm.k8s.io/v1beta1\nkind: ClusterConfiguration\nkubernetesVersion: v1.16.3\napiServer:\n  certSANs:\n    - \"k8s-apiserver-vip\"\ncontrolPlaneEndpoint: \"k8s-apiserver-vip:16443\"\nnetworking:\n  podSubnet: \"10.200.0.0/16\"\n```\n\n上述配置中，我们将api server设置为了api的域名。\n\n启动集群\n\n```bash\nsudo kubeadm init --config=./cluster-config.yaml --upload-certs\n```\n\n启动成功后，输出\n\n```bash\nYour Kubernetes control-plane has initialized successfully!\n\nTo start using your cluster, you need to run the following as a regular user:\n\n  mkdir -p $HOME/.kube\n  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config\n  sudo chown $(id -u):$(id -g) $HOME/.kube/config\n\nYou should now deploy a pod network to the cluster.\nRun \"kubectl apply -f [podnetwork].yaml\" with one of the options listed at:\n  https://kubernetes.io/docs/concepts/cluster-administration/addons/\n\nYou can now join any number of the control-plane node running the following command on each as root:\n\n  kubeadm join cluster.coder4:16443 --token vrvld4.vmo52532y6129ktg \\\n    --discovery-token-ca-cert-hash sha256:3c0268be210a9bd736eebaf80cc6d052b7b317251b15b76b88ddaa223a5836c2 \\\n    --control-plane --certificate-key 3c1890e1c1ceb1c64c7d1cd002e943e25c89f48c5b3b0e7d84d0879759e54545\n\nPlease note that the certificate-key gives access to cluster sensitive data, keep it secret!\nAs a safeguard, uploaded-certs will be deleted in two hours; If necessary, you can use\n\"kubeadm init phase upload-certs --upload-certs\" to reload certs afterward.\n\nThen you can join any number of worker nodes by running the following on each as root:\n\nkubeadm join cluster.coder4:16443 --token vrvld4.vmo52532y6129ktg \\\n    --discovery-token-ca-cert-hash sha256:3c0268be210a9bd736eebaf80cc6d052b7b317251b15b76b88ddaa223a5836c2\n```\n\n与之前的单master集群相比，这里的输出有两个join命令，其中前者是用于加入其他master节点的，而后者是用于普通节点加入的。\n\n我们先继续k1上的配置\n\n```bash\nmkdir -p $HOME/.kube\nsudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config\nsudo chown $(id -u):$(id -g) $HOME/.kube/config\n```\n\n配置网络：\n\n```bash\nwget https://docs.projectcalico.org/v3.9/manifests/calico.yaml\nsed -i -e \"s?192.168.0.0/16?10.200.0.0/16?g\" calico.yaml\nkubectl apply -f ./calico.yaml\n```\n\n如果你对上述网络配置不熟悉，请参考[《搭建Kubernetes集群》](k8s-cluster.md)一节中的介绍。\n\n接着在k2和k3执行master节点的加入：\n\n```bash\nsudo kubeadm join cluster.coder4:16443 --token vrvld4.vmo52532y6129ktg \\\n    --discovery-token-ca-cert-hash sha256:3c0268be210a9bd736eebaf80cc6d052b7b317251b15b76b88ddaa223a5836c2 \\\n    --control-plane --certificate-key 3c1890e1c1ceb1c64c7d1cd002e943e25c89f48c5b3b0e7d84d0879759e54545\n```\n\n和配置\n\n```bash\nmkdir -p $HOME/.kube\nsudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config\nsudo chown $(id -u):$(id -g) $HOME/.kube/config\n```\n\n经过上述配置后，我们检查一下，发现3台master已经成功启动了：\n\n```bash\nkubectl get nodes\nNAME   STATUS   ROLES    AGE     VERSION\nk1     Ready    master   2m47s   v1.16.3\nk2     Ready    master   90s     v1.16.3\nk3     Ready    master   86s     v1.16.3\n```\n\n上述kubectl命令可以在k1~k3任意一台机器执行。\n\n对于k4~k6，我们执行普通节点的加入就好：\n\n```bash\nsudo kubeadm join cluster.coder4:16443 --token vrvld4.vmo52532y6129ktg \\\n  --discovery-token-ca-cert-hash sha256:3c0268be210a9bd736eebaf80cc6d052b7b317251b15b76b88ddaa223a5836c2 \\\n  --control-plane --certificate-key 3c1890e1c1ceb1c64c7d1cd002e943e25c89f48c5b3b0e7d84d0879759e54545\n```\n\n经过上述操作，我们的集群启动完毕：\n\n```bash\nkubectl get nodes\nNAME   STATUS   ROLES    AGE     VERSION\nk1     Ready    master   42m     v1.16.3\nk2     Ready    master   37m     v1.16.3\nk3     Ready    master   9m39s   v1.16.3\nk4     Ready    <none>   3m25s   v1.16.3\nk5     Ready    <none>   3m18s   v1.16.3\nk6     Ready    <none>   3m15s   v1.16.3\n```\n\n为了验证高可用，我们可以试着关闭一下k1，发现虽然节点离线了，但是集群依然可以正常工作，如下所示：\n\n```bash\nkubectl get nodes\nNAME   STATUS     ROLES    AGE     VERSION\nk1     Ready      master   3h37m   v1.16.3\nk2     NotReady   master   3h33m   v1.16.3\nk3     Ready      master   3m18s   v1.16.3\nk4     Ready      <none>   178m    v1.16.3\nk5     Ready      <none>   178m    v1.16.3\nk6     Ready      <none>   178m    v1.16.3\n```\n\n至此，高可用Kubernetes集群已经搭建完毕。\n\n## 共有云下Kubernetes集群的HA方案\n\n在上一小节的开始，我们提到了“不要在共有云尝试上述方案”，为什么呢？\n\n如果你动手尝试过的话，就会发现keepalived在共有云上是行不通的，这是因为：\n\n- 禁用了ssrp，导致ip切换无法广播\n- 网卡和ip是存在映射绑定关系的，无法动态添加virtual ip\n\n上述限制主要是出于网络隔离性的安全性考量而产生的。\n\n为了在共有云上部署HA的Kubernetes集群，我们只能另辟蹊径。\n\n事实上，共有云多数提供了负载均衡服务（来替代keepalived），我们将直接使用它来搭建HA集群。\n\n我们假设在阿里云上有6台主机：\n\n- 192.168.8.208 k1(master)\n- 192.168.8.207 k2(master)\n- 192.168.8.209 k3(master)\n- 192.168.8.211 k4\n- 192.168.8.212 k5\n- 192.168.8.213 k6\n\n其中k1~k3依然作为master节点，其他节点作为普通节点，我们预留了192.168.8.210作为负载均衡的ip。\n\n### 启动HA集群\n\n首先，我们将k1和k3的api-server-lb都指向k1：\n\n```bash\ncat >> /etc/hosts << EOF\n192.168.8.208   k8s-apiserver-lb\nEOF\n```\n\n接着在k1上启动k8s集群：\n\n```bash\n# nano cluster-config.yaml\napiVersion: kubeadm.k8s.io/v1beta1\nkind: ClusterConfiguration\nkubernetesVersion: v1.16.3\napiServer:\n  certSANs:\n    - \"k8s-apiserver-lb\"\ncontrolPlaneEndpoint: \"k8s-apiserver-lb\"\nnetworking:\n  podSubnet: \"10.200.0.0/16\"\n```\n\n初始化：\n\n```bash\nsudo kubeadm init --config=./cluster-config.yaml --upload-certs\n```\n\n启动成功过后，也是同样返回了两组节点加入命令：\n\n```bash\nYour Kubernetes control-plane has initialized successfully!\n\nTo start using your cluster, you need to run the following as a regular user:\n\n  mkdir -p $HOME/.kube\n  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config\n  sudo chown $(id -u):$(id -g) $HOME/.kube/config\n\nYou should now deploy a pod network to the cluster.\nRun \"kubectl apply -f [podnetwork].yaml\" with one of the options listed at:\n  https://kubernetes.io/docs/concepts/cluster-administration/addons/\n\nYou can now join any number of the control-plane node running the following command on each as root:\n\n  kubeadm join k8s-ctl:6443 --token 3kmc18.dhw625244cp3fbc7 \\\n    --discovery-token-ca-cert-hash sha256:d01e9b50221c690bad534e2ec7b0d6054052f4a016390268730d0b32f387650f \\\n    --control-plane --certificate-key 849fff7f3df059ed87d91b82f00bdba5988268f9be08f6630eb6d3185438bd7c\n\nPlease note that the certificate-key gives access to cluster sensitive data, keep it secret!\nAs a safeguard, uploaded-certs will be deleted in two hours; If necessary, you can use\n\"kubeadm init phase upload-certs --upload-certs\" to reload certs afterward.\n\nThen you can join any number of worker nodes by running the following on each as root:\n\nkubeadm join k8s-ctl:6443 --token 3kmc18.dhw625244cp3fbc7 \\\n    --discovery-token-ca-cert-hash sha256:d01e9b50221c690bad534e2ec7b0d6054052f4a016390268730d0b32f387650f\n```\n\n我们先完成k1上的配置：\n\n```bash\nmkdir -p $HOME/.kube\nsudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config\nsudo chown $(id -u):$(id -g) $HOME/.kube/config\n```\n\n初始化网络组件：\n\n```bash\nwget https://docs.projectcalico.org/v3.9/manifests/calico.yaml\nsed -i -e \"s?192.168.0.0/16?10.200.0.0/16?g\" calico.yaml\nkubectl apply -f ./calico.yaml\n```\n\nk2、k3执行master加入操作：\n\n```bash\nsudo   kubeadm join k8s-ctl:6443 --token 3kmc18.dhw625244cp3fbc7 \\\n    --discovery-token-ca-cert-hash sha256:d01e9b50221c690bad534e2ec7b0d6054052f4a016390268730d0b32f387650f \\\n    --control-plane --certificate-key 849fff7f3df059ed87d91b82f00bdba5988268f9be08f6630eb6d3185438bd7c\n```\n\n和配置：\n\n```bash\nmkdir -p $HOME/.kube\nsudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config\nsudo chown $(id -u):$(id -g) $HOME/.kube/config\n```\n\n接着，我们在阿里云上配置一个私网的负载均衡器，设置如下：\n\n- IP: 192.168.8.210\n- 对外暴露TCP 6443端口\n- 默认服务器组k1、k2、k3，端口均为6443\n- 健康检查间隔调整至最低\n\n配置完成后，稍等几秒钟，执行telnet 负载均衡ip，发现成功连接：\n\n```bash\ntelnet 192.168.8.210 6443\nTrying 192.168.8.210...\nConnected to 192.168.8.210.\nEscape character is '^]'.\n\n\n```\n\n接下来，我们在k4~k6配置host为负载均衡的ip：\n\n```bash\ncat >> /etc/hosts << EOF\n192.168.8.210   k8s-apiserver-lb\nEOF\n```\n\n然后作为普通节点加入：\n\n```bash\nsudo kubeadm join k8s-ctl:6443 --token 3kmc18.dhw625244cp3fbc7 \\\n    --discovery-token-ca-cert-hash sha256:d01e9b50221c690bad534e2ec7b0d6054052f4a016390268730d0b32f387650f\n```\n\n目前在阿里云的负载均衡上有一些限制，当后端服务(k1~k3)自己连接负载均衡ip时，会出现无法连接的情况，而对于其他工作节点(k4~k6)是没有问题的。\n\n对于上述问题，我建议在k1~k3上将上述apiserver修改为自己的ip，即\n\n```bash\n# vim /etc/hosts\n192.168.8.207~209   k8s-apiserver-lb\n```\n\n我们尝试关闭k2节点，然后验证一下高可用，成功：\n\n```bash\nkubectl get nodes\nNAME   STATUS     ROLES    AGE     VERSION\nk1     Ready      master   3h37m   v1.16.3\nk2     NotReady   master   3h33m   v1.16.3\nk3     Ready      master   3m18s   v1.16.3\nk4     Ready      <none>   178m    v1.16.3\nk5     Ready      <none>   178m    v1.16.3\nk6     Ready      <none>   178m    v1.16.3\n```\n\n至此，我们完成了共有云环境下的Kubernetes集群的HA方案。\n\n## 拓展与思考\n\n1. 出于安全性的考虑，Kubernetes集群的节点加入默认只有1个小时的时间窗口，超过这一时间后，如何才能加入集群呢？\n2. 除了keepalived、负载均衡，你还知道其他Kubernetes集群的HA方案么？\n\n"
  },
  {
    "path": "legacy/k8s/k8s-intro.md",
    "content": "# Kubernetes 快速入门\n\n## Kubernetes中的基本操作单元\n\n为了适应复杂的业务需求，Kubernetes中内置了不同层级的操作单元：\n* Pod: Pod是Kubernetes的基本操作单元，也是应用运行的载体。如果你了解Docker的话，可以理解为Pod = 若干紧密相连的Docker + 数据卷。Pod中可能包含若干容器，它们是无法进行更细粒度的分割的，例如:微服务和它的日志收集进程。Pod内部的这些容器共享相同的资源(网络、进程通信、数据卷）\n* Replica Set：高可用、高性能是分布式系统中常见的问题。一般都可以采用增加冗余节点的方式解决，Replica Set通过标签关联Pod，并可以设置一个副本数，以实现微服务的冗余。\n* Deployment: Deployment描述了一个部署。在Kubernetes中，并不推荐直接启动Pod，也不推荐使用Replica Set，而建议直接使用Deployment。通过在Deployment中描述所期望的Pod、版本和副本数量，就可以实现管理、滚动升级、回滚、扩容、缩容等复杂的操作。Deployment与Pod并非是包含关系，而是相互独立的。Deployment通过“标签匹配”，可以关联若干Pod。\n* Service: 从字面意义理解，Service就是服务组。类似的，Service也是独立于Pod、Deployment概念。它也是通过“标签匹配”的方式关联若干Pod，并对外提供了统一的服务代理。通过访问统一服务代理，流量被自动分发到所有关联的Pod上，服务代理可以根据不同策略，进行负载均衡。如果你仔细阅读了[微服务架构概览](architecture/overview.md)，就会明白，Kubernetes的Service就是服务发现的一种实现方式。\n\n## minikube\nKubernetes提供了强大的集群管理功能，当然，它的集群环境的配置较为复杂，并非简短篇幅可以说清楚。\n\n本书的核心是微服务架构，而非Kubernetes的使用，因此，我们不会详细讲解k8s集群的配置。\n\n幸运的是，k8s为我们提供了minikube，它一个用于快速开发的单机k8s环境，拥有与k8s集群完全相同的功能。本章的剩余章节，我们将使用minikube来进行讲解。\n\n关于minikube的安装，可以参考官方的这篇[minikube安装教程](https://kubernetes.io/docs/tasks/tools/install-minikube/)，这里不做详细展开。\n\n需要特别指出：minikube只限于开发和学习使用。对于生产环境，请务必配置Kubernetes的分布式集群，大家可以参考[官方文档](https://kubernetes.io/docs/home)。\n\n## Hello Deployment\n\nminikube安装妥当后，让我们来部署第一个Deployment。\n\n首先，启动minikube。第一次启动需要下载ISO镜像，时间较长，请耐心等待一下。\n```shell\nminikube start --disk-size 50g --memory 4096 --insecure-registry \"192.168.99.0/24\"\n```\n\n上述第一次start实际是配置了minikube虚拟机的参数，我们简单解释一下:\n* disk-size 磁盘空间我们给了50g。我们之后会配置私有Maven仓库，需要建立主仓库索引，默认的20g不太够用。\n* memory 内存，给了4G，可以根据你的需求自己设定。\n* insecure-registry 我们之后会搭建不带证书的私有仓库，所以这里预先设置好仓库的IP范围。\n\n提醒一下，上述minikube参数只对第一次启动(实际是创建)生效，一旦虚拟机生成完毕，这些参数的修改都不会生效了。\n\nKubernetes支持两种操作方式：命令行参数、yaml文件定义。鉴于维护性等角度，我们更推荐推荐后者，即用yaml文件的方式。\n\nDeployment描述文件，lmsia-abc-server-deployment.yaml\n```yaml\napiVersion: apps/v1\n// Deployment\nkind: Deployment\nmetadata:\n  name: lmsia-abc-server-deployment\nspec:\n  selector:\n    matchLabels:\n      app: lmsia-abc-server\n  replicas: 2\n// Pod define\n  template:\n    metadata:\n      labels:\n        app: lmsia-abc-server\n    spec:\n      containers:\n      - name: lmsia-abc-server-ct\n        image: coder4/lmsia-abc-server:1.0\n        ports:\n        - containerPort: 8080\n        - containerPort: 3000\n```\n\n我们来解读一下这个yaml文件，采用自底向上的步骤：\n* Pod定义：如注释标记，文件的下半部分定义了Pod信息。\n　* metadata.labels.app是Pod的标签名，用于与Deployment、Replica Set和Service做关联。\n  * spec.containers定义了Pod的名字(name)、镜像(image)和开放端口(ports)。这里使用了我预先编译好的一个微服务镜像，它集成了REST服务和RPC服务，分别监听8080端口和3000端口。\n* Deployment定义：文件的上半部分，是部署的定义。\n * kind类型是Deployment\n * metadata.name是Deployment的名字，用于后续的进一步操作\n * replica是副本数定义，这里我们的副本数(replicas)设定为2。\n * selector.matchLabels定义了与Pod的关联，请注意selector.matchLabels与Pod中的metadata.labels需要保持一致，才能成功关联。\n\n理解了文件内容后，让我们来新建这个部署：\n```shell\nkubectl apply -f ./lmsia-abc-server-deployment.yaml\n\n```\n\n我们来看看启动了哪些Pod。这里我们以标签为查询参数。\n```shell\nkubectl get pods -l app=lmsia-abc-server\n\nNAME                                          READY     STATUS    RESTARTS   AGE\nlmsia-abc-server-deployment-bd4949ff9-jcczg   1/1       Running   0          10m\nlmsia-abc-server-deployment-bd4949ff9-zqsvc   1/1       Running   0          10m\n```\n\n不难发现，启动了两个Pod，和我们在yaml文件中设定的副本数一致。\n\n注意：上图展示的是最终结果，Pod的启动前需要先拉取镜像，因此会存在“非Running”的中间状态。\n\n截至目前，我们已经通过Deployment的方式，成功的启动了两个Pod。前面已经介绍，镜像中的微服务对外暴露了两个端口：REST(HTTP)服务的8080端口，和RPC服务的3000端口。接下来，我们尝试访问Pod内的HTTP服务。\n\n先来获取一下IP地址，以第一个Pod为例：\n\n```shell\nkubectl describe pod lmsia-abc-server-deployment-bd4949ff9-jcczg\n\nName:           lmsia-abc-server-deployment-bd4949ff9-jcczg\n....\nStatus:         Running\nIP:             172.17.0.5\nControlled By:  ReplicaSet/lmsia-abc-server-deployment-bd4949ff9\n....\n```\n\n由于结果较多，这里只截取了关键的几行，可以从结果中看到，名字为“lmsia-abc-server-deployment-bd4949ff9-jcczg”的Pod，它的IP是“\"172.17.0.5\"。\n\n尝试访问一下，会报\"No route to host\"错误：\n```shell\ncurl http://172.17.0.5:8080/lmsia-abc/api/\n\ncurl: (7) Failed to connect to 172.17.0.5 port 8080: No route to host\n```\n为什么会这样呢？因为我们启动的minikube集群实际是一个虚拟机，172.17.0.5是虚拟机的内网地址。我们执行命令的命令行，是在虚拟机外部。相当与我们从外网要访问内网地址，这自然是无法成功访问的。\n要说明的是，我们这里并没有访问跟路径，而是访问了“lmsia-abc/api/”，大家可以暂且认为这是一个合法的url pattern，我们将在微服务开发中对此进行讲解。\n\n如何解决呢，有两种方案：\n* 登录到虚拟机上，再访问\n* 打通内网和外网\n其中，第二种方案是日常工作中常见的需求，我们在OpenVPN + NAT 打通办公网与IDC](devops/openvpn-nat.md)一节中，详细介绍了一种方案。在此处，我们先采用第一种方案。\n\n登录到minikube虚拟机很简单：\n```shell\nminikube ssh\n\n$\n\n```\n\n注意：若需要登录到minikube虚拟机后再执行的操作，我们会增加一个$符号，以便区分。\n\n登录到minikube虚拟机后，再次尝试Pod上的HTTP服务，可以成功访问了：\n```shell\n$curl http://172.17.0.5:8080/lmsia-abc/api/\n\nHello, REST\n```\n\n至此，我们成功的创建了Deployment、查看了Pod的信息、访问了Pod上的Rest服务。\n\n最后，我们学习下如何删除Deployment。在虚拟机环境下，是没有kubectl可以使用的，所以首先要退出虚拟机。\n```shell\n$exit\n```\n\n然后再来删除Deployment，命令可以成功执行：\n```shell\nkubectl delete deployment lmsia-abc-server-deployment\n\ndeployment.extensions \"lmsia-abc-server-deployment\" deleted\n```\n\n再来看一下相关的Pod信息，发现已经找不到对应Pod了：\n```shell\nkubectl get pods -l app=lmsia-abc-server\n\nNo resources found.\n```\n"
  },
  {
    "path": "legacy/k8s/k8s-ipvs.md",
    "content": "# 为Kubernetes开启ipvs\n\n通过前面的章节，我们已经学会了如何部署一个真正的Kubernetes集群。\n\n此外，你可能也听说过，Kubernetes内置了Service、Deployment等机制，原生支持了负载均衡。\n\nKubernetes的负载均衡支持iptables、ipvs两种方案。\n\n两种负载均衡方案相比：ipvs不仅提供了更好的可拓展性，更高的性能，也支持服务器健康检查和重连功能。\n\n然而，Kubernetes默认采用iptables方案，我们在本节探讨如何在k8s中启用ipvs负载均衡。\n\n## 内核模块加载\n\nipvs依赖一些内核模块，我们先要进行加载\n\n```bash\nsudo modprobe ip_vs\nsudo modprobe ip_vs_rr\nsudo modprobe ip_vs_wrr\nsudo modprobe ip_vs_sh\nsudo modprobe nf_conntrack_ipv4\n```\n\n如果你不想每次都执行这些操作，也可以加入到/etc/modules文件中\n\n## 创建一个Kubernetes集群\n\n我们先使用常规方法创建一个集群，它依然是应用iptables来做负载均衡，稍后我们会修改这一点。\n\n如果你还不知道如何创建集群，可以参考[搭建Kubernetes集群](ms-discovery/k8s-cluster.md)\n\n搭建后，你可以验证一下当前的负载均衡技术，在主节点上执行：\n\n```bash\ndocker ps | grep proxy\n```\n\n获得id后，查看日志，例如我本地的是72a851c1f395\n\n```bash\ndocker logs -f 72a851c1f395\nW1126 11:49:10.647711       1 server_others.go:329] Flag proxy-mode=\"\" unknown, assuming iptables proxy\n```\n\n可以看到目前采用的是iptables\n\n## 修改为ipvs负载均衡\n\n我们依然在主节点上执行：\n\n```bash\nkubectl edit cm kube-proxy -n kube-system\n```\n\n找到\"mode\"一项，默认应该是空的，修改为\"ipvs\"。\n\n修改完毕后，不会自动生效，我们手动重启下kube-proxy节点：\n\n```bash\nkubectl get pod -n kube-system | grep kube-proxy |awk '{system(\"kubectl delete pod \"$1\" -n kube-system\")}'\n```\n\n稍等几秒钟，所有pod会自动重启，我们验证一下：\n\n```\nkubectl get pod -n kube-system |grep kube-proxy\nkube-proxy-8zh86             1/1     Running   0          60s\nkube-proxy-dp9lj             1/1     Running   0          58s\nkube-proxy-zd8xn             1/1     Running   0          52s\n```\n\n启动成功后，我们再次找到本地的kube-proxy的docker\n\n```bash\ndocker logs -f be8714395739\n....\nI1206 11:03:38.105777       1 node.go:135] Successfully retrieved node IP: 192.168.8.168\nI1206 11:03:38.105820       1 server_others.go:176] Using ipvs Proxier.\nW1206 11:03:38.106134       1 proxier.go:420] IPVS scheduler not specified, use rr by default\n....\n```\n\n可以看到，已经成功切换到ipvs负载均衡。\n\n我们也可以通过ipvsadm验证一下：\n\n```bash\nsudo ipvsadm\nIP Virtual Server version 1.2.1 (size=4096)\nProt LocalAddress:Port Scheduler Flags\n  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn\nTCP  k1:https rr\n  -> k1:6443                      Masq    1      5          0\nTCP  k1:domain rr\n  -> 10.32.0.2:domain             Masq    1      0          0\n  -> 10.32.0.3:domain             Masq    1      0          0\nTCP  k1:9153 rr\n  -> 10.32.0.2:9153               Masq    1      0          0\n  -> 10.32.0.3:9153               Masq    1      0          0\nUDP  k1:domain rr\n  -> 10.32.0.2:domain             Masq    1      0          0\n  -> 10.32.0.3:domain             Masq    1      0          0\n```\n\n## 拓展与思考\n\n1. ipvs支持多种负载均衡策略，在Kubernetes集群中，如何调整这些策略呢？\n2. 想较于iptable，为什么ipvs可以提供更好的性能呢？"
  },
  {
    "path": "legacy/k8s/k8s-office.md",
    "content": "# 办公网与Kubernetes集群的打通\n\n通过前面的章节，我们已经学会了Kubernetes集群的搭建、优化、部署应用。\n\n在本节中，我们将讨论另一个常见的场景：打通办公网与Kubernetes集群。\n\n在测试或者开发环境，经常会有这种需求：\n\n- 从开发机直接调用k8s集群中的微服务的rpc接口\n- 从开发机直接访问k8s集群中的数据库\n\n然而在默认情况下，办公网络和Kubernetes的pod(service)网络时不互通的。\n\n如果只有少量的HTTP服务，我们可以通过ingress来解决问题。但对于mysql、redis、rpc接口等，就无法通过ingress了，只能用NodePort，当微服务数量不断扩大时，端口将变得难以维护。\n\n对于这种需求，可行的方案并不多，我们将通过vpn来打通办公网和Kubernets集群的网络。\n\n此外，由于绝大多数场景中，只需要从办公网访问Kubernetes集群，所以我们只讨论这种单向打通。\n\n## 从办公网访问Kubernetes集群的Pod IP\n\n首先，你需要有一个Kubernetes集群（calico网络组件），我们假设集群有4个节点：\n\n- k1: 192.168.8.174，主节点\n- k2: 192.168.8.175，普通节点\n- k3: 192.168.8.176，普通节点\n- k4: 192.168.8.177，普通节点（不调度pod）\n\n另外，我们的Kubernetes集群部署在共有云上，至少要保证k4节点有公网IP。\n\n我们查看下状态：\n\n```bash\nkubectl get nodes\nNAME   STATUS   ROLES    AGE     VERSION\nk1     Ready    master   7m35s   v1.16.3\nk2     Ready    <none>   72s     v1.16.3\nk3     Ready    <none>   48s     v1.16.3\nk4     Ready    <none>   37s     v1.16.3\n```\n\n我们看一下pod的网段：\n\n```bash\nkubectl cluster-info dump | grep -m 1 cluster-cidr\n    \"--cluster-cidr=10.200.0.0/16\",\n```\n\n再看一下service的网段：\n\n```\nkubectl cluster-info dump | grep -m 1 service-cluster-ip-range\n    \"--service-cluster-ip-range=10.96.0.0/12\",\n```\n\n上述两个网段，下面会用到。\n\n我们会将vpn部署在k4上，所以先要打一个标记，不调度Pod到k4上：\n\n```bash\nkubectl taint nodes k4 forward=k4:NoSchedule\n```\n\n接下来，我们将在k4上部署openvpn。\n\n由于众所周知的原因，我这里不详述openvpn配置了，但请注意一下几点：\n\n- 假设vpn的网络段是192.168.6.0/24\n- openvpn可以直接用docker启动，并需要绑定到k4机器的公网ip的端口上\n\n此外，客户端的ovpn配置中，要包含以下几行：\n\n```bash\n# For Inner Network (Can Be Done at openwrt also)\nroute 192.168.6.0 255.255.255.0\nroute 192.168.8.0 255.255.255.0\nroute 10.96.0.0 255.240.0.0\nroute 10.200.0.0 255.255.0.0\n```\n\n这4个IP段的配置，分别表示了vpn、k8s物理机、pod ip、service ip。\n\nvpn隧道建立连接后，我们在办公网机器ping一下k8s集群中的pod：\n\n```bash\n# ping\nping 10.200.0.2\nPING 10.200.0.2 (10.200.0.2): 56 data bytes\n64 bytes from 10.200.0.2: icmp_seq=0 ttl=61 time=10.682 ms\n64 bytes from 10.200.0.2: icmp_seq=1 ttl=61 time=10.746 ms\n64 bytes from 10.200.0.2: icmp_seq=2 ttl=61 time=10.970 ms\n```\n\n至此，办公网 -> k8s的IP访问已经打通。\n\n温馨提示：如果你觉得让开发每次挂openvpn不方便：\n\n- 可以将openvpn直接部署到软路由上。\n- 如果你的k8s部署在共有云上，也可以使用云厂提供的vp专线(接到出口路由上)替换openvpn。\n\n## 打通DNS\n\nKubernetes内置了DNS服务，可以通过域名来访问Service。\n\n我们假设通过helm在k8s中部署了一套memcached服务。\n\n首先获取一下dns的地址：\n\n```bash\nkubectl  get svc  -n kube-system |grep kube-dns\nkube-dns   ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   39m\n```\n\n在vpn隧道建立连接后，从办公网尝试直接dig：\n\n```bash\ndig @10.96.0.10 test-memcached.default.svc.cluter.coder4\n\n; <<>> DiG 9.10.6 <<>> @10.96.0.10 test-memcached.default.svc.cluter.coder4\n; (1 server found)\n;; global options: +cmd\n;; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 56004\n;; flags: qr aa rd; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1\n;; WARNING: recursion requested but not available\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 4096\n;; QUESTION SECTION:\n;test-memcached.default.svc.cluter.coder4. IN A\n\n;; ANSWER SECTION:\ntest-memcached.default.svc.cluter.coder4. 30 IN\tA 10.42.0.1\ntest-memcached.default.svc.cluter.coder4. 30 IN\tA 10.36.0.1\ntest-memcached.default.svc.cluter.coder4. 30 IN\tA 10.44.0.1\n\n;; Query time: 12 msec\n;; SERVER: 10.96.0.10#53(10.96.0.10)\n;; WHEN: Tue Dec 17 20:24:21 CST 2019\n;; MSG SIZE  rcvd: 237\n```\n\n发现可以找到对应的3个后台POD，说明这套DNS基本是可以使用的。\n\n但在实际应用开发中，我们不太可能反复切换本地开发机的DNS服务器地址。\n\n因为，我的建议是：\n\n- 办公网内用dnsmasq搭建一套内网DNS，并打通隧道\n- dnsmasq配置，默认走公网dns ip\n- dnsmasq配置，对于default.svc.cluster.coder4等k8s集群内的域名，直接走k8s的dns ip\n\n由于这些配置比较常规，这里就不再赘述了。\n\n至此，我们完成了办公网 -> kubernetes集群的网络打通。\n\n## 拓展与思考\n\n1. 如果想做双向打通，如何修改配置呢？\n2. 如何用云厂商提供的虚拟专线隧道(VPC)，替换openvpn呢？\n3. 如果Kubernetes集群部署在办公网内，但属于不同的网段。能否利用双网卡+iptables来替换openvpn呢？"
  },
  {
    "path": "legacy/ms-circuit-breaker-and-limit/README.md",
    "content": "# 微服务熔断与限流\n\n\"熔断\"、\"限流\"这两个词看起来是和性能相关的。你可能会有疑问：微服务架构支持横向拓展，性能不够加机器就可以，为什么还需要熔断呢？\n\n是的，横向拓展确实可以提升性能，但同时也降低了可用性。假设一个服务的可用性是99.9%，假设有100个服务实例依赖这个微服务，那么整体的可用性就0.999^100=90%，可用性下降了接近10！考虑到微服务架构下的性能横向拓展，微服务有多个副本的情况下，100个实例的依赖是很正常的情况。\n\n微服务的实际应用中，调用链条可能更长，A调用B、B调用C...链条上的任何一个服务发生故障，都会导致调用链条上后续服务发生故障，从而将故障的影响逐级放大，最终导致整个系统崩溃，这称为雪崩效应。\n\n为了避免雪崩的发生，除了提高服务的稳定性外，还可以采取\"熔断\"、\"限流\"等防御性手段。\n\n本章将就这两种手段进行讨论，并引入Hytrix和Guava两款开源解决方案，探讨如何在微服务架构下快速地实现服务的熔断和限流。\n"
  },
  {
    "path": "legacy/ms-circuit-breaker-and-limit/sb-hystrix.md",
    "content": "# 熔断与Hystrix\n\n在本节，我们将讨论\"熔断\"方案的思路及其在微服务架构下的落地。\n\n\"熔断\"这个词来源于电路保护。如果一条线路上的的电压过高，就会将保险丝烧断，从而切断该条线路上的电流，防止其影响其他线路。\n\n我们将上述场景对应到微服务上，当调用某个微服务频繁发生故障(相当于电压过高)，会触发熔断(相当于保险丝烧断)，微服务将直接返回一个降级的结果，防止影响其他业务。\n\n故障的类型可能有很多种，最常见的是抛出了异常或者调用超时。\n\n你可能会有疑问：返回一个降级的结果，不就是错误了么？\n\n是的，降级结果是错误的。但你可以降低错误的影响范围，例如，返回上一次成功执行的结果。\n\n## Hystrix的基本用法\n\nHystrix是由Netflex开源的一款开源组件，提供了基础的熔断功能。\n\nHystrix将降级策略封装在Commend中，不同的Commend根据group分割开。Commend内置了run和fallback两个方法，内置方法。\n\n正常情况下，会先执行run方法（正常执行逻辑），若发生了故障，再执行fallback方法并返回其结果。若发生多次故障会在一定时间范围内触发短路，即跳过run方法，直接执行fallback方法。\n\n关于Hystrix的更详细的原理，可以参考[Hystrix工作原理（官方文档翻译）](https://segmentfault.com/a/1190000012439580)，这里不再赘述。\n\n有几个涉及Hystrix的关键参数，这里做一些简要介绍:\n首先是几个key\n* groupKey: 区分不同降级环境，相同的groupKey下处在相同的降级环境。\n* commendKey: 区分不同命令。 \n* threadPoolKey: 相同的key将运行在相同的线程池下。\n然后是几个通用配置\n* executionTimeoutInMilliseconds: 执行超时时间，单位毫秒\n* circuitBreakerEnabled: 发生多次故障后，是否会触发短路。默认是false，即总是先执行run方法，不会主动跳过。\n最后是线程池\n* coreSize: 线程池常驻线程数量\n* maximumSize: 线程池最大线程数量\n* allowMaximumSizeToDivergeFromCoreSize: 仅当设置为true时，上述maxiumSize才生效。\n* maxQueueSize: 最多允许多少个任务堆积\n* queueSizeRejectionThreshold: 多少个任务堆积会处罚降级。堆积的任务太多，说明处理速度跟不上需求了，也会被认为是故障并处罚降级。\n\n## Hystrix的基本封装\n\n上述概念固然重要，但每次做熔断时如果都要仔细考虑，未免太过繁琐，为此，我们做了一个抽象的基类，实现了上述的默认的配置:\n```java\npackage com.coder4.lmsia.hystrix;\n\nimport com.coder4.sbmvt.trace.TraceIdContext;\nimport com.coder4.sbmvt.trace.TraceIdUtils;\nimport com.netflix.hystrix.HystrixCommand;\nimport com.netflix.hystrix.HystrixCommandGroupKey;\nimport com.netflix.hystrix.HystrixCommandKey;\nimport com.netflix.hystrix.HystrixCommandProperties;\nimport com.netflix.hystrix.HystrixThreadPoolKey;\nimport com.netflix.hystrix.HystrixThreadPoolProperties;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.util.StringUtils;\n\nimport java.util.function.Supplier;\n\n/**\n * @author coder4\n */\npublic class BaseHystrixCommend<R> extends HystrixCommand<R> {\n\n    private Logger LOG = LoggerFactory.getLogger(getClass());\n\n    private final Supplier<R> realSupplier;\n\n    private final Supplier<R> fallbackSupplier;\n\n    public BaseHystrixCommend(String key, Supplier<R> realSupplier, Supplier<R> fallbackSupplier) {\n        this(key, new BaseHytrixConfig(), realSupplier, fallbackSupplier);\n    }\n\n    public BaseHystrixCommend(String key, BaseHytrixConfig config, Supplier<R> realSupplier, Supplier<R> fallbackSupplier) {\n        super(Setter\n                // 3个key合一\n                .withGroupKey(HystrixCommandGroupKey.Factory.asKey(key))\n                .andCommandKey(HystrixCommandKey.Factory.asKey(key))\n                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(key))\n                .andCommandPropertiesDefaults(\n                        HystrixCommandProperties.Setter()\n                                .withExecutionTimeoutInMilliseconds(config.getExecutionTimeoutInMilliseconds())\n                                .withCircuitBreakerEnabled(config.isCircuitBreakerEnabled())\n                                .withFallbackIsolationSemaphoreMaxConcurrentRequests(config.getFallbackIsolationSemaphoreMaxConcurrentRequests())\n                )\n                .andThreadPoolPropertiesDefaults(\n                        HystrixThreadPoolProperties.defaultSetter()\n                                .withAllowMaximumSizeToDivergeFromCoreSize(config.isAllowMaximumSizeToDivergeFromCoreSize())\n                                .withCoreSize(config.getCorePoolSize())\n                                .withMaximumSize(config.getMaxPoolSize())\n                                .withMaxQueueSize(config.getMaxQueueSize())\n                                .withQueueSizeRejectionThreshold(config.getMaxQueueSize())\n                ));\n        this.realSupplier = realSupplier;\n        this.fallbackSupplier = fallbackSupplier;\n    }\n\n    @Override\n    protected R run() throws Exception {\n        if (StringUtils.isEmpty(TraceIdContext.getTraceId())) {\n            TraceIdContext.setTraceId(TraceIdUtils.getTraceId());\n        }\n        R r = this.realSupplier.get();\n        TraceIdContext.removeTraceId();\n        return r;\n    }\n\n    @Override\n    protected R getFallback() {\n        try {\n            LOG.error(\"enter fallback because \", getExecutionException());\n            return this.fallbackSupplier.get();\n        } finally {\n            TraceIdContext.removeTraceId();\n        }\n    }\n\n}\n```\n\n如上所示，在构造函数中，对上述参数进行了配置，此外还结合了[分布式追踪](../ms-log/sb-trace.md)中介绍的TraceIdContext，注入并销毁traceId。\n\nHystrix的默认值在另一个文件中进行了配置:\n\n```java\npackage com.coder4.lmsia.hystrix;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\n/**\n * @author coder4\n */\n@Data\n@Builder\n@NoArgsConstructor\n@AllArgsConstructor\npublic class BaseHytrixConfig {\n\n    private static int DEFAULT_EXECUTION_TIMEOUT_IN_MILLISECONDS = 1000;\n\n    private static int DEFAULT_FALL_BACK_ISOLATION_SEMAPHORE_MAX_CON_CURRENT_REQUESTS = 512;\n\n    private static int DEFAULT_CORE_POOL_SIZE = 64;\n\n    private static int DEFAULT_MAX_POOL_SIZE = 512;\n\n    private static int DEFAULT_MAX_QUEUE_SIZE = 32;\n\n    // 执行时限(毫秒)\n    private int executionTimeoutInMilliseconds\n            = DEFAULT_EXECUTION_TIMEOUT_IN_MILLISECONDS;\n\n    // 启动断路器\n    private boolean circuitBreakerEnabled = true;\n\n    // (信号量隔离时)降级调用最大并发数\n    private int fallbackIsolationSemaphoreMaxConcurrentRequests\n            = DEFAULT_FALL_BACK_ISOLATION_SEMAPHORE_MAX_CON_CURRENT_REQUESTS;\n\n    // 允许线程数峰值超过coreSize\n    private boolean allowMaximumSizeToDivergeFromCoreSize = true;\n\n    // 核心线程数\n    private int corePoolSize = DEFAULT_CORE_POOL_SIZE;\n\n    // 最大线程数量\n    private int maxPoolSize = DEFAULT_MAX_POOL_SIZE;\n\n    // 最大队列等待数量\n    private int maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;\n}\n\n```\n\n有了上述默认设置后，我们将上述两个类封装成独立的项目lmsia-hystrix中，方便其他微服务的调用。\n\n## Hystrix在微服务中的用法\n\n最后，我们来看一下如何在微服务中使用hystrix。\n\n首先在依赖中引入lmsia-hystrix:\n```\ncompile 'com.github.liheyuan:lmsia-hystrix:0.0.4'\n```\n\n然后定义一个Commend，并execute\n```\n    @GetMapping(value = \"/\")\n    public String hello() {\n        return new BaseHystrixCommend<String>(\"abc\", this::helloReal, this::helloFallback).execute();\n    }\n\n    private String helloReal() {\n        LOG.info(\"hello real\");\n        if (true) {\n            throw new RuntimeException(\"haha\");\n        }\n        return abcLogic.getHello();\n    }\n\n    private String helloFallback() {\n        LOG.info(\"hello fb\");\n        return \"Hello, fallback\";\n    }\n```\n\n如上所示，我们定义了两个函数helloReal是正常逻辑，但这个方法会抛异常并触发降级。helloFallback是降级逻辑。\n\n我们构造的Commend会根据情况执行上述两个方法。\n\n在未启用Commend前，rest请求会直接500错误，因为抛出了异常。\n\n启用Commend后，返回总是200。前几次，会发现日志先打印\"hello real\"，再打印\"hello fb\"，这说明只是出发降级未触发短路。当多执行几次，就会发现不再输出\"hello real\"，这时就是真正触发了短路。\n\n至此，我们已经借助Hystrix实现了微服务的降级功能。\n\n## 拓展与思考\n1. 返回一个固定的降级结果，可能会影响产品体验。如果想返回上一次执行成功的结果，该如何进行修改呢，Hystrix中有没有内置这个功能呢？\n2. Hystrix中默认触发短路的阈值是多少，默认短路时间又是多少呢， 如果想进行修改，需要如何进行配置呢？\n"
  },
  {
    "path": "legacy/ms-circuit-breaker-and-limit/sb-limit.md",
    "content": "# 限流的实现\n\n与\"熔断\"类似，\"限流\"也是一种降级手段，但限流的思路更简单、直观: 它直接拒绝部分请求。\n\n在微服务架构下，若大量请求超过微服务的处理能力时，可能会将服务打跨，甚至产生雪崩效应、影响系统的整体稳定性。\n\n孙子兵法有一计\"李代桃僵\"，说的是当局势发展到必然有所损失时，应当舍得局部弱小兵力，以保全大局优势。\n\n我们可以将这种战略应用到微服务中，在流量超出承受阈值时，直接进行\"限流\"、拒绝部分请求，从而保证系统的整体稳定性。\n\n有的业务场景中，系统压力并不大，但也需要限制用户每秒的操作次数，例如：验证码的发送接口。\n\n进行限流的方案有很多种，本节这里讨论两个层面上的限流：负载均衡器和微服务。\n\n## 负载均衡层的限流\n\nNginx是一款高性能的反向代理服务器，是用户请求进入系统的第一道关卡。\n\n在Nginx上配置限流策略，不仅可以保护系统稳定性，也能防范一部分恶意攻击。\n\n我们来看一组最常见的策略。\n\n(1) 按照IP地址, 限制每秒请求数量:\n\n```\nlimit_req_zone $binary_remote_addr zone=limit1:1m rate=20r/s;\n```\n\n如上所述，配置的是一个限流区域:\n* 区域名字是limit1，分配1MB的内存，大致可以追踪1.5万个IP地址\n* 每个IP地址，每秒钟的访问上限是20次。\n\n(2) 支持突发缓存队列\n\n```\nlimit_req zone=limit1 burst=10 nodelay;\n```\n\n如上所述，细化了limit1这个区域上的具体策略:\n* burst建立了一个长度为10的缓冲区，若突发流量导致限流会先放到缓冲区中\n* nodelay当缓冲区已满了，丢弃请求，返回503\n\n上面提到的缓存策略可以应用于全局，也可以应用于不同的url路径下。\n\n此外，Nginx还提供了多种高级的限流配置手段，可以参考这篇博客[Nginx Rate Limiting](https://dzone.com/articles/nginx-rate-limiting)。\n\n## 微服务层的基础限流\n\n由于Nginx无法解析业务逻辑，只能在IP层面进行\"较为粗犷的限流\"。\n\n如果想结合业务逻辑或更复杂的策略，可以在微服务层面进行限流。\n\nGuava是谷歌开源的Java库，其中提供了基于令牌桶算法的RateLimiter。我们将以此为基础，实现微服务层面的限流。\n\n首先，来定义一个注解类\n```java\npackage com.coder4.lmsia.ratelimit;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 对方法限流，超限会抛出HTTP 429异常\n * @author coder4\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.METHOD})\n@Documented\npublic @interface MethodRateLimit {\n\n    // 每秒允许多少次请求\n    double permitsPerSecond();\n}\n```\n\n如上所述，注解定义了一个参数permitsPerSecond，即每秒允许几次请求，支持非整数配置。\n\n为了让注解生效，我们需要配合AOP使用：\n```java\npackage com.coder4.lmsia.ratelimit.aspect;\n\nimport com.coder4.lmsia.commons.http.exception.Http429TooManyRequestsException;\nimport com.coder4.lmsia.ratelimit.MethodRateLimit;\nimport com.coder4.lmsia.ratelimit.RateLimiterProvider;\nimport com.google.common.util.concurrent.RateLimiter;\nimport org.aspectj.lang.ProceedingJoinPoint;\nimport org.aspectj.lang.annotation.Around;\nimport org.aspectj.lang.annotation.Aspect;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.stereotype.Component;\n\nimport java.util.Optional;\n\n/**\n * @author coder4\n */\n@Component\n@Aspect\npublic class MethodRateLimitAspect {\n\n    protected Logger LOG = LoggerFactory.getLogger(getClass());\n\n    @Around(value = \"(execution(* com.coder4..*(..))) && @annotation(methodLimit)\", argNames = \"joinPoint, methodLimit\")\n    public Object methodAround(ProceedingJoinPoint joinPoint, MethodRateLimit methodLimit)\n            throws Throwable {\n        // Get RateLimiter\n        Optional<RateLimiter> rateLimiterOp = RateLimiterProvider.getInstance()\n                .getRateLimiter(\n                        joinPoint.getSignature().toLongString(), methodLimit.permitsPerSecond());\n        if (!rateLimiterOp.isPresent() || rateLimiterOp.get().tryAcquire()) {\n            // allow\n            return joinPoint.proceed();\n        } else {\n            // deny\n            throw new Http429TooManyRequestsException();\n        }\n    }\n\n}\n```\n\n如上所述，我们对所有添加了MethodRateLimit注解的方法进行AOP注入:\n* 根据方法名获取一个RateLimiter，RateLimiterProvider稍后会介绍\n* 若可以获得令牌，则执行方法，否则抛出HTTP429(Too Mangy Requests)异常\n\n再来看一下RateLimiterProvider:\n```java\npackage com.coder4.lmsia.ratelimit;\n\nimport com.google.common.cache.Cache;\nimport com.google.common.cache.CacheBuilder;\nimport com.google.common.util.concurrent.RateLimiter;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Optional;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * @author coder4\n */\npublic class RateLimiterProvider {\n\n    private Logger LOG = LoggerFactory.getLogger(getClass());\n\n    private static final RateLimiterProvider instance = new RateLimiterProvider();\n\n    private static final int CAPACITY = 2000;\n\n    private static final int TTL_SECS = 60;\n\n    private Cache<String, RateLimiter> rateLimiterCache;\n\n    private RateLimiterProvider() {\n        rateLimiterCache = CacheBuilder.newBuilder()\n                .maximumSize(CAPACITY)\n                .expireAfterAccess(TTL_SECS, TimeUnit.SECONDS)\n                .build();\n    }\n\n    public static RateLimiterProvider getInstance() {\n        return instance;\n    }\n\n    public Optional<RateLimiter> getRateLimiter(String key, double permitsPerSecond) {\n        // 未测试线程安全，但影响不大\n        try {\n            return Optional.ofNullable(\n                    rateLimiterCache.get(key, () -> RateLimiter.create(permitsPerSecond)));\n        } catch (ExecutionException e) {\n            LOG.error(\"getRateLimiter exception\", e);\n            return Optional.empty();\n        }\n    }\n\n}\n```\n\n如上所述，Provider的内部使用Guava的Cache机制:\n* 根据字符串key从Cache中尝试获取RateLimiter,获取不到则新建一个\n* Cache最高存储2000个、过期时间为60秒，以防不断膨胀导致过高的内存开销。\n\n有了上述注解，在微服务中进行限流将异常简单:\n```java\n    @MethodRateLimit(permitsPerSecond = 2.0)\n    @GetMapping(value = \"/\")\n    public String hello() {\n        return new BaseHystrixCommend<String>(\"abc\", this::helloReal, this::helloFallback).execute();\n    }\n```\n\n如上，只需要一行代码即可搞定。\n\n## 微服务层的高级限流\n\n在一些复杂的业务场景下，我们可能希望根据不同用户或其他字段进行限流。\n\n我们提供了另一款MethodParamRateLimit来满足这类需求:\n```java\npackage com.coder4.lmsia.ratelimit;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * 根据方法+参数限流，超限会抛出HTTP 429异常\n *\n * @author coder4\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.METHOD})\n@Documented\npublic @interface MethodParamRateLimit {\n\n    // 每秒允许多少词请求\n    double permitsPerSecond();\n\n    // 参数下标(0开始）\n    int paramIndex();\n}\n```\n\n新增的参数paramIndex稍后会作出解释，我们看一下AOP的Aspect:\n```java\npackage com.coder4.lmsia.ratelimit.aspect;\n\nimport com.coder4.lmsia.commons.http.exception.Http429TooManyRequestsException;\nimport com.coder4.lmsia.ratelimit.MethodParamRateLimit;\nimport com.coder4.lmsia.ratelimit.RateLimiterProvider;\nimport com.google.common.util.concurrent.RateLimiter;\nimport org.aspectj.lang.ProceedingJoinPoint;\nimport org.aspectj.lang.annotation.Around;\nimport org.aspectj.lang.annotation.Aspect;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.stereotype.Component;\n\nimport java.util.Optional;\n\n/**\n * @author coder4\n */\n@Component\n@Aspect\npublic class MethodParamRateLimitAspect {\n\n    protected Logger LOG = LoggerFactory.getLogger(getClass());\n\n    @Around(value = \"(execution(* com.coder4..*(..))) && @annotation(methodParamLimit)\", argNames = \"joinPoint, methodParamLimit\")\n    public Object methodAround(ProceedingJoinPoint joinPoint, MethodParamRateLimit methodParamLimit)\n            throws Throwable {\n        // Get RateLimiter\n        Optional<RateLimiter> rateLimiterOp = RateLimiterProvider.getInstance()\n                .getRateLimiter(getRateLimiterKey(joinPoint, methodParamLimit), methodParamLimit.permitsPerSecond());\n        if (!rateLimiterOp.isPresent() || rateLimiterOp.get().tryAcquire()) {\n            // allow\n            return joinPoint.proceed();\n        } else {\n            // deny\n            throw new Http429TooManyRequestsException();\n        }\n    }\n\n    private String getRateLimiterKey(ProceedingJoinPoint joinPoint, MethodParamRateLimit methodParamLimit) {\n\n        // Get Param Value\n        String paramValue = getParamLimit(joinPoint, methodParamLimit.paramIndex());\n\n        return String.format(\"%s-%s\", joinPoint.getSignature().toString(), paramValue);\n    }\n\n    private String getParamLimit(ProceedingJoinPoint joinPoint, int paramIndex) {\n        Object[] args = joinPoint.getArgs();\n        if (paramIndex < 0 || paramIndex >= args.length) {\n            LOG.warn(\"paramIndex exceed length, use default\");\n            return \"default_param\";\n        }\n        return args[paramIndex].toString();\n    }\n\n}\n```\n\n如上所述，进行切面处理时:\n* 从用方法和第paramIndex参数的值拼接为key来获取RateLimit。这有些抽象，我们稍后会举个例子。\n* 其他处理策略同MethodLimitAspect\n\n看一下用法:\n```java\n    @MethodParamRateLimit(permitsPerSecond = 1, paramIndex = 0)\n    @GetMapping(value = \"/ids/{id}\")\n    public String helloWithId(@PathVariable int id) {\n        return helloFallback(id);\n    }\n```\n\n如上所述，MethodParamRateLimit应用在此处，实现了根据不同的id进行限流，每个id每秒只能访问1次，不同id之间不会相互影响。\n\n## 阅读与思考\n1. Nginx进行限流时，容易发生误伤，例如来自内网或者监控系统的IP。请自行查找资料，实现白名单配置，避免这种情况。\n2. 除了负载均衡、微服务层面的限流，你还能想到其他层面的限流么？\n"
  },
  {
    "path": "legacy/ms-config/README.md",
    "content": "# 微服务配置中心\n\n\"配置中心\"是对配置进行集中管理的系统，是微服务架构中的一个基础环节。\n\n服务端存在多种类型的配置:\n* 环境变量：如操作系统环境变量\n* 内部配量：如阈值、关键字\n* 应用配量：如功能、特性开关\n\n环境变量等配置很少发生变动，但内部、应用配置可能会频繁发生变动。\n\n传统的配置都是写在文件中，并伴随服务端一起发布，这样做有一些弊端:\n* 配置文件格式多样，易产生错误。例如，yaml和ini的格式就极容易混淆，导致配置出错。\n* 不同环境下配置容易混淆。例如，误将测试环境配置发布到生产环境。\n* 每次配置的修改都要伴随服务端的上线。在微服务架构下，服务种类、实例数量增多，为了改动配置而单独上线会产生较大的运维成本。\n\n针对这些场景，配置中心应运而生，它的核心功能有:\n* 提供配置的存、取服务\n* 支持配置的动态更新，即无需启动服务即可完成配置更新。\n* 支持配置的版本管理或追溯功能，方便进行审计。\n\n本章将围绕配置中心展开讨论，在第一节讨论了一种基于cfg4j的配置中心方案。第二节讨论了如何在Spring Boot中整合上述配置中心。\n"
  },
  {
    "path": "legacy/ms-config/cfg4j.md",
    "content": "# cfg4j及方案简介\n\n实现微服务的配置中心有多种选择方案，常见的方案有:\n* 使用Spring Cloud全家桶中的Spring Cloud Config。\n* 使用Consul或者Zookeeper作为分布式一致性存储，自己实现配置中心。\n\n但上述方案都有一些不足:\n* Sping Cloud Config不支持配置的实时更新，需要额外实现。此外，Spring Cloud的依赖较多，不太干净。\n* Consul或者Zookeeper只提供了存取接口，对于配置下发、更新(特别是配置的管理界面)都需要自己开发实现，成本非常高。\n\n我们在此选用了一种成本较低的方案: 使用cfg4j库，存储源选择git。\n\ncfg4j是一款\"分布式系统\"的配置类库，它不包含服务端存储部分，但实现了从多种数据源读取，更新配置，以及缓存策略。\n\n我们选用git作为数据源，原因有:\n* 本书架构本身采用git作为代码管理工具，不需要额外部署成本\n* 本书已经采用了gerrit作为git服务器和代码审核工具，它的diff、review功能非常强大，可以不用额外开发配置管理的web界面\n\n## gerrit中的权限配置\n\n截至目前的最新版，cfg4j默认只支持从匿名git仓库拉取配置，我们需要对gerrit进行一些配置以满足这一条件。\n\n使用管理员帐号登录gerrit，然后选择\"Projects\" -> \"All-Projects\" -> \"Access\"，进行如下修改:\n* 给\"Reference: refs/*\" 添加匿名组 \"Anonymous Users\"　的 \"DENY\"权限。注意，这只是一个全局的默认配置，可以被项目级别的权限覆盖。\n* 新建项目\"lmsia-config\"，修改项目权限，给\"Reference: refs/*\"添加匿名组 \"Anonymous Users\"　的 \"ALLOW\"权限。\n\n经过上述修改后，我们需要做一下简单验证:\n```shell\ngit clone http://127.0.0.1:9002/lmsia-config\n```\n\n如果上述命令可以直接clone项目到本地，且无需输入用户名、密码，说明gerrit的权限配置成功。\n\n## 支持多个微服务的配置\n\n在前面，我们新建了项目lmsia-config，并且给它配置了匿名访问权限。\n\n我们的微服务可能有很多，如何让lmsia-config项目，支持多个微服务的配置呢？\n\n我们通过目录的方式来实现:\n```shell\n.\n├── lmsia-abc\n│   └── config.properties\n└── lmsia-xyz\n    └── config.properties\n```\n\n如上所示，对于每一个微服务项目，我们都在lmsia-config下创建一个目录，并在目录中放置config.properties作为配置文件。\n\n在后面的章节，我们将介绍如何让微服务自动地解析上述配置文件路径。\n\n## 拓展与思考\n1. 如果测试环境、线上环境需要使用不同的配置，如何支持这种特性？(提示：分支）\n2. 如果希望匿名用户只读而不能写，如何修改gerrit权限？\n"
  },
  {
    "path": "legacy/ms-config/consul-devops.md",
    "content": "# Consul服务的运维\n\nConsul是一款支持高可用的服务发现、配置管理服务。我们使用Consul作为配置中心的基础服务。即由Consul提供配置的管理、获取等基础功能。\n\n在探讨配置中心之前，我们首先来看一下Consul的运维工作。\n\n## 生成Consul所需要的证书\n\n本文配置生成的部分参考了[consul-on-kubernetes](https://github.com/kelseyhightower/consul-on-kubernetes)项目。\n\n首先生成中证书\n\n```shell\n~/go/bin/cfssl gencert -initca ca/ca-csr.json | ~/go/bin/cfssljson -bare ca\n\n~/go/bin/cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca/ca-config.json -profile=default ca/consul-csr.json | ~/go/bin/cfssljson -bare consul\n```\n\n生成的文件为:\n* ca-key.pem\n* ca.pem\n* consul-key.pem\n* consul.pem\n\n然后根据证书生成kubernetes所需要的configmap\n```shell\nkubectl create secret generic consul --from-literal=\"gossip-encryption-key=X9u61NBsxoQt6edwxpStLg==\" --from-file=ca.pem --from-file=consul.pem --from-file=consul-key.pem\n\nkubectl create configmap consul --from-file=configs/server.json\n```\n\n其中上述密码部分是由“consul keygen”生成的，可以改成自己的密码。\n"
  },
  {
    "path": "legacy/ms-config/sb-config.md",
    "content": "# Spring Boot整合配置中心\n\n上一小节中，我们探讨了如何利用gerrit搭建配置中心的版本仓库。\n\n现在，我们探讨如何在Spring Boot的框架中整合配置中心。\n\n## 开发lmsia-cfg4j库，实现配置项的自动注入\n\n与之前的Cache等功能类似，我们在多个微服务中，轻松地使用配置中心，所以将相关功能提取但独立的项目中。你可以在这里查看[lmsia-cfg4j](https://github.com/liheyuan/lmsia-cfg4j)的源代码。\n\n如前文描述，我们使用cfg4j来辅助实现配置中心的功能。\n\ncfg4j提供了默认的\"Binding\"方式，来绑定配置项到类中，但使用起来较为繁琐。\n\n许多Spring的功能都是通过注解来实现的，非常方便。我们的配置项也可以用注解来实现，首先定义一个注解接口:\n\n```java\npackage com.coder4.lmsia.cfg4j;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * @author coder4\n */\n@Target({ElementType.FIELD, ElementType.PARAMETER})\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\npublic @interface Cfg4jValue {\n\n    String value() default \"\";\n}\n\n```\n\n如上所示，之后希望可以使用类似\"@Cfg4jValue\"的方式，将配置项注解到对应字段中。\n\n有了注解接口，如何实现自动注解呢，传统的方式需要使用动态代理来完成，在这里我们采用Spring提供的BeanPostProcessor来完成：\n```java\npackage com.coder4.lmsia.cfg4j;\n\nimport org.cfg4j.provider.ConfigurationProvider;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.aop.support.AopUtils;\nimport org.springframework.beans.BeansException;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.config.BeanPostProcessor;\nimport org.springframework.core.Ordered;\nimport org.springframework.util.ReflectionUtils;\n\nimport java.lang.reflect.Field;\nimport java.util.NoSuchElementException;\n\n/**\n * @author coder4\n */\npublic class Cfg4jValueProcessor implements BeanPostProcessor, Ordered {\n\n    private Logger LOG = LoggerFactory.getLogger(getClass());\n\n    @Autowired\n    private ConfigurationProvider configurationProvider;\n\n    // 初始化前注入\n    @Override\n    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {\n\n        final Class targetClass = AopUtils.getTargetClass(bean);\n        ReflectionUtils.doWithFields(targetClass, field -> process(bean, targetClass, field), field -> {\n            return field.isAnnotationPresent(Cfg4jValue.class);\n        });\n        return bean;\n    }\n\n    private void process(final Object bean, Class<?> targetClass, final Field field) {\n        // Get injected field name\n        Cfg4jValue valueAnnotation = field.getDeclaredAnnotation(Cfg4jValue.class);\n        String fieldName = getPropName(valueAnnotation, field.getName());\n        // inject for some support type\n        fieldSetWithSupport(bean, field, fieldName);\n    }\n\n    private void fieldSetWithSupport(Object bean, Field field, String key) {\n        Class type = field.getType();\n        field.setAccessible(true);\n        try {\n            if (int.class == type || Integer.class == type) {\n                field.set(bean, configurationProvider.getProperty(key, Integer.class));\n            } else if (boolean.class == type || Boolean.class == type) {\n                field.set(bean, configurationProvider.getProperty(key, Boolean.class));\n            } else if (String.class == type) {\n                field.set(bean, configurationProvider.getProperty(key, String.class));\n            } else if (long.class == type || Long.class == type) {\n                field.set(bean, configurationProvider.getProperty(key, Long.class));\n            } else {\n                LOG.error(\"not support cfj4j value inject type\");\n                throw new RuntimeException(\"not supported cfg4jValue type\");\n            }\n        } catch (IllegalAccessException e) {\n            LOG.error(\"exception during field set\", e);\n            throw new RuntimeException(e);\n        } catch (NoSuchElementException e) {\n            LOG.error(\"config missing key, please check\");\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static String getPropName(Cfg4jValue annotation, String defaultName) {\n        String key = annotation.value();\n        if (key == null || key.isEmpty()) {\n            key = defaultName;\n        }\n        return key;\n    }\n\n    @Override\n    public Object postProcessAfterInitialization(Object bean, String beanName) throws\n            BeansException {\n        return bean;\n    }\n\n    @Override\n    public int getOrder() {\n        return HIGHEST_PRECEDENCE;\n    }\n}\n```\n\n如上所示，Cfg4jValueProcessor完成了以下功能:\n* 自动查找所有@Cfg4jValue的注解\n* 对有上述注解的字段，根据字段名从Cfg4j的数据源(ConfigurationProvider)中读取配置项\n* 若有配置项，完成类型转换并注入到对应字段中。这里目前只支持int, long, string这三种类型。\n\nConfigurationProvider是cfg4j的数据源，如前文所述，我们希望它自动从gerrit来读取。\n\n为此，实现一个自动配置如下:\n```java\npackage com.coder4.lmsia.cfg4j.configuration;\n\nimport com.coder4.lmsia.cfg4j.Cfg4jValueProcessor;\nimport org.cfg4j.provider.ConfigurationProvider;\nimport org.cfg4j.provider.ConfigurationProviderBuilder;\nimport org.cfg4j.source.ConfigurationSource;\nimport org.cfg4j.source.context.environment.Environment;\nimport org.cfg4j.source.context.environment.ImmutableEnvironment;\nimport org.cfg4j.source.context.filesprovider.ConfigFilesProvider;\nimport org.cfg4j.source.git.GitConfigurationSourceBuilder;\nimport org.cfg4j.source.reload.ReloadStrategy;\nimport org.cfg4j.source.reload.strategy.PeriodicalReloadStrategy;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.stereotype.Service;\n\nimport java.nio.file.Paths;\nimport java.util.Arrays;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * @author coder4\n */\n@Configuration\n@ConditionalOnProperty(\"msName\")\npublic class Cfg4jGitConfiguration {\n\n    @Value(\"${msName}\")\n    private String msName;\n\n    // May Change this\n    private static String CONFIG_GIT_HOST = \"10.1.64.72\";\n\n    // May Change this\n    private static String CONFIG_GIT_REPO = \"http://\" + CONFIG_GIT_HOST + \":9002/lmsia-config.git\";\n\n    // May Change this\n    private static String branch = \"master\";\n\n    private static int RELOAD_SECS = 60;\n\n    @Bean\n    public ConfigurationProvider configurationProvider() {\n        ConfigFilesProvider configFilesProvider = () -> Arrays.asList(Paths.get(msName + \"/config.properties\"));\n        ConfigurationSource source = new GitConfigurationSourceBuilder()\n                .withRepositoryURI(CONFIG_GIT_REPO)\n                .withConfigFilesProvider(configFilesProvider)\n                .build();\n\n        Environment environment = new ImmutableEnvironment(branch);\n\n        ReloadStrategy reloadStrategy = new PeriodicalReloadStrategy(RELOAD_SECS, TimeUnit.SECONDS);\n\n        return new ConfigurationProviderBuilder()\n                .withConfigurationSource(source)\n                .withEnvironment(environment)\n                .withReloadStrategy(reloadStrategy)\n                .build();\n    }\n\n    @Bean\n    public Cfg4jValueProcessor createCfg4jValueProcessor() {\n        return new Cfg4jValueProcessor();\n    }\n\n}\n```\n\n上述完成了如下功能:\n* 从Git仓库拉去lmsia-config项目（即前文用于微服务配置的仓库）\n* 定义缓存时间为60秒\n* 配置文件具体路径为/项目名/config.properties(与前一节的目录结构相对应)\n* 顺便配置刚才编写的Cfg4jValueProcessor，让配置可以自动注入到对应的地方上。\n\n当然，为了让上述自动注解生效，不要忘记配置spring.factories\n```\norg.springframework.boot.autoconfigure.EnableAutoConfiguration=com.coder4.lmsia.cfg4j.configuration.Cfg4jGitConfiguration\n```\n\n至于项目名，可以通过微服务自身的application.yaml指定，若不制定将不会启动这个自动配置\n\n## 使用\n\n有了lmsia-cfg4j后，如何在微服务中自动注入配置项目呢？\n\n首先我们需要准备好配置，例如lmsia-config/lmsia-abc的config.properties中定义\n```\nkey=value\nenable=false\n```\n\n接着，在微服务的application.yaml中定义项目名称\n```yaml\nmsName: lmsia-abc\n```\n\n最后一步，在代码中，使用注解：\n```java\npackage com.coder4.lmsia.abc.server.configuration;\n\nimport com.coder4.lmsia.cfg4j.Cfg4jValue;\nimport lombok.Data;\nimport org.springframework.stereotype.Service;\n\n/**\n * @author coder4\n */\n@Service\n@Data\npublic class TestConfig {\n\n    @Cfg4jValue\n    private String key;\n\n    @Cfg4jValue\n    private boolean enable;\n\n}\n```\n\n如上所示，是不是非常简单！\n\n你可以启动自己的微服务项目，测试上述配置项是否被如期的注入进来。\n\n在lmsia-cfg4j中，有默认60秒的缓存，你也可以修改lmsia-abc的配置，等待60秒，再观察新的配置是否生效。\n\n## 拓展与思考\n\n1. 我们介绍的的配置中心架构中，实际采用的是拉默认来获取最新配置。当微服务及其副本数量众多的时候，可能会对gerrit服务器造成巨大压力。有什么好的方法可以改进这一点么?\n2. 如果需要结合profile实现测试、线上环境使用不同的配置，lmsia-cfg4j项目要如何进行修改呢？ \n"
  },
  {
    "path": "legacy/ms-delivery/README.md",
    "content": "# 微服务持续交付\n\n\"持续集成\"(Continuous integration)：频繁地将开发代码合并到主干，并保证可以编译通过，并通过基本额单元测试。\n坚持持续集成的优点有：\n* 快速失败(Fast Fail): 尽早发现错误，\"早发现、早治疗\"，将错误的成本降到最低。\n* 减少代码冲突风险：使用频繁、多次、小的分支合并，来很久不合并代码导致的大量的代码冲突。\n* 提升迭代速度和质量：小步快跑，让进度更可控，避免\"Deadline前赶工期\"的现象。\n\n\"持续交付\"(Continuous delivery)：频繁地将代码的最新版本，交付给用户（或线上环境），它的优势不言而喻：\n* 更快的交付速度：借助自动化的持续交付系统，可以做到1天上线多次\n* 产品功能可并行上线：持续交付降低了上线的人力成本，多个功能可并行开发、上线\n\n在互联网软件开发领域，持续集成和持续交付已经成为基本的共识，极大地提升了项目的迭代、交付速度。\n\n与之形成鲜明对比的是，在传统软件开发领域，持续集成和持续交付的理念还没有得到贯彻，软件的开发、上线以周、月为单位，并且功能之间往往难以进行拆分。\n\n本章将围绕上述两个问题，探讨探讨微服务架构下，借助Jenkins实现持续集成、持续部署。\n"
  },
  {
    "path": "legacy/ms-delivery/jenkins-devops.md",
    "content": "# Jenkins构建平台的运维\n\nJenkins是一款开源的持续构建工具，除了基础功能外，还有各种功能丰富的插件，可以实现各种高级功能。\n\nJenkins常见的应用场景是:\n* 项目的自动构建(编译)，即持续集成\n* 自动执行项目的单元/集成测试，即持续测试\n* 实现项目的自动部署、上线，即持续部署\n\n在本小节，我们首先探讨Jenkins系统的运维工作，并尝试将Jenkins与LDAP系统集成起来。\n\n## Jenkins系统的搭建\n\n对于Jenkins系统，我们将直接用Docker部署在物理机而不是k8s集群上，主要原因有:\n* Jenkins的定位是持续集成、持续部署，需要作用在k8s集群上，故耦合不宜太紧密。\n* 由于资源有限，我们的k8s集群主要运行微服务及相关后台组件。\n\n我们先来看一下创建脚本\n```shell\n#!/bin/bash\nNAME=\"jenkins\"\nVOLUME=\"$HOME/docker_data/jenkins\"\n\n# ensure volume ready\nsudo mkdir -p $VOLUME\nsudo chmod -R 777 $VOLUME\n\n# submit to local docker node \ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --name $NAME \\\n    -v $VOLUME:/var/jenkins_home \\\n    -p 9001:8080 \\\n    -p 50000:50000 \\\n    --detach \\\n    --restart always \\\n    jenkins/jenkins:2.60.3-alpine\n```\n\n如上所述:\n* 我们使用了jenkins的官方镜像\n* 映射默认端口到本地的9001，这个即web管理界面的端口\n* 映射端口50000，这个是用于管理通信的端口\n* volume映射了/var/jenkins_home文件夹\n\n启动完毕后，需要进行Jenkins系统的初始化，如下图所示：\n\n![Jenkins初始化](./jenkins-init.png)\n\n初始化需要输入初始化密码，存放在docker的/var/jenkins_home/secrets/initialAdminPassword目录下，可以通过docker exec -i -t 登录容器，然后cat查看。\n```shell\ndocker exec -i -t ad74be122fcd /bin/sh\n\ncat /var/jenkins_home/secrets/initialAdminPassword\n0b7aee7513774800ac6cf1fdd41d0366\n```\n\n输入密码后，稍等一会儿，提示选择安装插件的模式，选择自定义插件：\n\n![Jenkins插件选项初始化](./jenkins-init-plugin.png)\n\n针对本书架构，建议选择的插件为：\n* Organization and Administration\n * Folder\n* Build Features\n * Build Timeout\n * SSH Agent\n * Timestamper \n * Workspace Cleanup \n * Active Choice\n* Build Tools\n * Gradle\n* Pipelines and Continuous Delivery\n * Pipeline\n * Pipeline: Stage View\n* Source Code Management\n * Git\n* Distributed Builds \n * SSH Slaves\n* User Management and Security\n * LDAP\n * Role-based Authorization Strategy\n* Notifications and Publishing\n * Publish Over SSH\n * SSH\n\n选择好后进行安装，这一步会耗费的久一些，随后需要创建管理员帐号：\n\n![Jenkins创建管理员帐号](./jenkins-init-admin.png)\n\n最后点击完成\n\n经过上述配置后，我们已经构建了基本的Jenkins环境，并有了一个可以登录的系统管理员帐号。\n\n## Jenkins系统接入LDAP系统\n\n作为持续集成的核心，Jenkins系统的重要性毋庸置疑，有必要进行细粒度的权限控制与管理，例如：\n* 开发用户可以修改、执行Job，但不能新建项目\n* 测试用户只能运行Job\n* 管理员可以进行任何操作\n\n因此，我们需要给每个团队成员配置独立的帐号。面对这种需求，接入LDAP验证是一个很好的选择。\n\n在[LDAP 内部账号管理系统](toolchain/ldap.md)一节中，我们介绍了LDAP的运维方案，本节假设你已经部署好了LDAP服务。\n\n登录Jenkins后，在左侧菜单选择\"Manage Jenkins\"，随后选择\"Configure Global Security\"\n\n在\"Access Control\"中选中\"LDAP\"，填写如下信息:\n* Server: LDAP服务和端口号，例如 ldap.coder4.com:389\n* Root DN: LDAP的根目录，例如 dc=coder4,dc=com\n* User search base: 用户所在的子目录，例如 ou=users\n* User search filter: 用户名的字段，例如cn={0}\n* Manager DN: 可查询LDAP的额外帐号，例如一个只读帐号 cn=guest,dc=coder4,dc=com\n* Manager Password: 上述帐号对应的密码\n* Display Name LDAP attribute: 帐号名字段名，例如cn\n* Email Address LDAP attribute: 邮箱字段名，例如mailk\n\n上述都填写完毕后，可以点击底部的Test LDAP Settings\n\n测试通过后，不要忘记点击页面最底部的保存按钮。\n\n随后，我们尝试用LDAP帐号登录，可以发现登录成功。\n\n## 设置基于角色的权限\n\n团队成员有多种不同的角色，如前面提到的开发、测试、管理员。\n\n在Jeknins中，可以设置不同的角色，并针对角色配置不同的权限。\n\n点击\"Manage Jenkins\"，然后选择\"Configure Global Security\"，在\"Authorization\"中更改为\"Role Based Strategy\"。\n\n不要退出Jenkins，选择\"Manage and Assign Roles\"，首先管理角色\"Manage and Assign Roles\"\n\n![Jenkins分配系统角色](./jenkins-role.png)\n\n如上图所示，新增2个角色：开发rd和测试qa，并设置对应的权限。\n\n随后进入\"Assign Roles\"，将开发帐号赋给对应的角色，这里给lihy赋予admin角色、给zhangsan赋予rd角色，如图所示：\n\n![Jenkins赋予系统角色](./jenkins-assign-role.png)\n\n我们可以登录zhangsan，没有看到\"Manage Jenkins\"的菜单，角色配置成功。\n\n"
  },
  {
    "path": "legacy/ms-delivery/ms-cd.md",
    "content": "# Jenkins持续部署\n\n在上一小节，我们完成了Jenkins的持续集成工作，经过持续集成，我们的代码已经编译成Docker镜像，并被Push到私有仓库中。\n\n在本节，我们接着前一小节的成功，讨论部署问题。\n\n这里的部署指的是将微服务真正地运行在k8s集群系统中，主要涉及如下步骤:\n* 获取可部署的Docker镜像版本\n* 获取k8s集群操作权限\n* 服务操作: 部署服务、重启服务等\n\n其中获取k8s集群的权限有多种方式:\n* 通过REST API\n* 远程登录k8s的master节点\n\n其中通过REST API的方式更加可靠、易于编程，但在k8s 1.7版本后，新增了权限控制，使用起来较为复杂。\n\n在这里，我们采用直接登录k8s节点的方式。\n\n## 获取Docker镜像的版本列表\n\n前面已经提到，在持续集成时，会打包好最新的镜像到仓库中，并且制定版本为Jenkins的版本。\n\n对于部署环节，却不一定总是需要上线最新版本的镜像。例如，我们上线了一个新功能，半小时后发现有个Bug，需要回滚，此时就需要上线前一个版本的镜像。\n\n如何获取镜像对应的版本呢？我们可以通过\"Active Choices Plugin\"插件来实现。\n\n首先用管理员登录，Manage Jenkins -> Manage Plugins，安装\"Active Choices Plugin\"插件。\n\n随后新建一个项目，例如\"lmsia-xyz-deploy\"，勾选\"This project is parameterized\"，然后新增一个\"Active Choices Parameter\"，进行如下配置:\n\n![配置动态参数](./jenkins-docker-img-version.png)\n\n其中的主要代码如下:\n```groovy\n// Variable\ndef proj_name=\"lmsia-xyz\"\ndef curl_prefix=\"https://10.1.64.72/v2/$proj_name\"\n\n// Get From Docker Registry Using curl\nimport groovy.json.JsonSlurper\ndef cmd= \"curl --insecure $curl_prefix/tags/list\"\ndef object = new JsonSlurper().parseText(cmd.execute().text)\n\n// Sort and return\nreturn object.tags.sort { a, b -> b.compareToIgnoreCase a }\n```\n\n上述代码从Docker私有仓库获取\"lmsia-xyz\"这个镜像的所有版本，然后倒着排序后，返回给Jenkins插件。\n\n点击底部保存，点击\"lmsia-xyz-deploy\"项目的\"Build with Parameters\"，可以发现正常显示了版本列表:\n\n![正常展示了版本列表](./jenkins-deploy-version.png)\n\n## 新增远程主机\n\n前面已经提到，我们将直接登录到k8s的主节点来执行部署命令。\n\n为了实现这一点，首先要将该机器添加到Jenkins的SSH Site中。\n\n用管理员帐号登录 -> Manage Jenkins -> Configure System, 找到SSH Site添加如下:\n\n![配置远程执行主机](./jenkins-ssh-succ.png)\n\n除了配置主机名、端口外，记得选择一个已经配置好的SSH私钥，这需要提前到Jenkins的Credentials中配置好。\n\n接着我们回到lmsia-xyz-deploy项目，在Build环节新建一个\"Execute script on remote host using ssh\"\n\n* SSH Site选择刚才配置好的主机\n* Command里设置为\"/home/coder4/deploy2k8s.sh $JOB_NAME $IMG_VERSION\"\n\n其中deploy2k8s.sh需要部署在这台远程的/home/coder4目录下（根据你的情况可自行更改），内容为:\n```bash\n#!/bin/bash\nset -e\n\nif [ x\"$#\" != x\"2\" ];then\n    echo \"Usage $0 proj_name img_version\"\n    exit -1\nfi\n\n# Const\nDOCKER_REGISTRY=\"10.1.64.72\"\nPROJECT_NAME=$1\nIMAGE_NAME=$(echo $PROJECT_NAME | sed -r 's/-deploy$//g')\nIMAGE_VERSION=$2\n\n# Generate yaml \ncat > ./deployment.yaml <<EOF\n\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: $IMAGE_NAME-deployment\nspec:\n  selector:\n    matchLabels:\n      app: $IMAGE_NAME \n  replicas: 2\n  template:\n    metadata:\n      labels:\n        app: $IMAGE_NAME\n    spec:\n      containers:\n      - name: $IMAGE_NAME-ct\n        image: $DOCKER_REGISTRY/$IMAGE_NAME:$IMAGE_VERSION\n        ports:\n        - containerPort: 8080\n        - containerPort: 3000\n\nEOF\n\n# Deploy \nkubectl apply -f ./deployment.yaml\n\n```\n\n上述代码完成如下功能:\n* 获取镜像名称和本次要上线的镜像版本\n* 生成deployment模板\n* 调用kubectl 部署\n\n至于重启服务和停止服务，我们作为思考题，交给读者来实现。\n\n至此，我们完成了持续部署的工作。\n\n## 拓展与思考\n1. 微服务的持续部署，除了部署更新版本，还需要停止或者重启，如何实现这一点，请结合本书介绍的内容，结合网上的其他资料，自行实现。\n1. 上述模板只是简单的部署，如果需要实践前面章节介绍的微服务发现模型，需要进行哪些修改呢？\n"
  },
  {
    "path": "legacy/ms-delivery/ms-ci.md",
    "content": "# Jenkins持续集成\n\n在本小节中，我们将讨论持续集成。\n\n持续集成指的是: 频繁地（一天多次）将代码集成到主干。这里的集成不止是代码合并，还要保证可以通过编译、单元测试、集成测试。\n\n持续集成的主要优点是:\n* 快速发现错误。即所谓的\"早发现、早治疗\"。\n* 减少开发、主分支之间的冲突。更频繁的分支合并，可以降低分支冲突发生的概率。\n* 为持续部署打下基础。代码先要能集成起来，才能够进一步地部署。\n\n针对我们当前的架构，持续集成分为如下几个步骤:\n* 从Gerrit上check out代码\n* Gradle编译项目\n* 打包Docker镜像\n\n## 部署Salve打包机\n\n在持续构建的过程中，需要迁出代码、编译、打Docker镜像等步骤，如果都放在Jenkins的容器内执行，存在一些缺点：\n* 影响Jenkins主服务性能。编译、打镜像都是非常耗费系统资源的操作。如果放到Jenkins上执行，势必会影响服务的流畅性和稳定性。\n* 耦合过紧，不方便升级维护。如果要Jenkins支持打包、编译，需要自己定制镜像，即在Jenkins基础上安装Gradle、Java等工具。如果将来任何一个工具或Jenkins需要升级版本，就需要重新开发镜像，非常繁琐。\n\n因此，通常都会新建一个独立于Jenkins的打包机，里面集成编译打包工具。Jenkins将打包机作为Slave添加到系统中，在打包时将调用Slave机器执行打包任务。\n\nSlave打包机可以采用物理机、虚拟机或者容器，这里我们选择容器的方式，主要优点有:\n* 故障恢复，方便运维。编译、打包的复杂程度较高，经常会导致打包机挂起，采用容器的方式，可以快速恢复故障。\n* 版本可控，方便升级。持续集成的工具链需要逐步升级。例如Java、Gradle版本，每年都会更新几次，采用容器的方式，可以更好地管理版本，更精细地控制打包流程。\n* 资源使用可控。编译、打包耗费较大资源，有时候为了保证整个系统稳定性，要限制打包使用的CPU、内存资源，使用容器技术可以轻松地做到这一点。\n* 启动快速，方便扩展。随着持续集成的规模逐渐扩大，要同时执行多个任务，甚至要在打包高峰期，动态启动若干Slave，以提升并行度。这类似于微服务的副本扩展，容器集群(如k8s)对这种副本拓展有很好地支持。\n\n我们首先来构建一个打包机的镜像,Dockerfile如下:\n```shell\nFROM ubuntu:18.04\n\n# apt-add-repostory zip unzip git\nRUN apt-get update\nRUN apt-get install -y apt-utils software-properties-common zip unzip git\n\n# SSH\nRUN apt-get install -y openssh-server \\\n    && mkdir /var/run/sshd \\\n    && sed -ri 's/UsePAM yes/#UsePAM yes/g' /etc/ssh/sshd_config\n\n# Java\nENV JAVA_HOME /usr/lib/jvm/java-8-oracle\nRUN \\\n  echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | debconf-set-selections && \\\n  add-apt-repository -y ppa:webupd8team/java && \\\n  apt-get update && \\\n  apt-get install -y oracle-java8-installer\n\n# Gradle\nENV GRADLE_HOME /opt/gradle\nENV GRADLE_VERSION 4.10\nARG GRADLE_DOWNLOAD_SHA256=248cfd92104ce12c5431ddb8309cf713fe58de8e330c63176543320022f59f18\nRUN set -o errexit -o nounset \\\n\t&& echo \"Downloading Gradle\" \\\n\t&& wget --no-verbose --output-document=gradle.zip \"https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip\" \\\n\t\\\n\t&& echo \"Checking download hash\" \\\n\t&& echo \"${GRADLE_DOWNLOAD_SHA256} *gradle.zip\" | sha256sum --check - \\\n\t\\\n\t&& echo \"Installing Gradle\" \\\n\t&& unzip gradle.zip \\\n\t&& rm gradle.zip \\\n\t&& mv \"gradle-${GRADLE_VERSION}\" \"${GRADLE_HOME}/\" \\\n\t&& ln --symbolic \"${GRADLE_HOME}/bin/gradle\" /usr/bin/gradle\n\n# Create User\nRUN useradd -m build \nRUN echo \"build:build123\" | chpasswd\n\n# Docker ce\nRUN apt-get install -y \\\n    apt-transport-https \\\n    ca-certificates \\\n    curl \\\n    software-properties-common\nRUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -\nRUN add-apt-repository \\\n    \"deb [arch=amd64] https://download.docker.com/linux/ubuntu \\\n    $(lsb_release -cs) \\\n    stable\"\nRUN apt-get install -y docker-ce\nRUN usermod -aG docker build\n\n# Clean\nRUN apt-get remove -y apt-utils software-properties-common && \\\n    apt-get autoremove -y && \\\n    apt-get clean && \\\n    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/cache/oracle-jdk8-installer\n\nEXPOSE 22\n\n# SSH Daemon\nCMD [\"/usr/sbin/sshd\", \"-D\"]\n\n```\n\n如上所示，上述镜像主要完成了以下功能:\n* JDK 8的安装\n* SSHD服务的安装\n* Gradle 4.10的安装\n* build用户(密码build123)的配置\n* Docker的安装和配置(这里是Docker inside Docker，我们只安装可执行文件，通过volulme映射使用物理机的docker)\n\n有了上述镜像后，我们启动这个slave容器:\n```shell\n#!/bin/bash\n\n# BUILD\ndocker build -t slave .\n\nNAME=\"slave\"\n\n# submit to local docker node \ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --name $NAME \\\n    -v /var/run/docker.sock:/var/run/docker.sock \\\n    -p 22 \\\n    --detach \\\n    --restart always \\\n    -d slave:latest \n\n```\n\n接下来，我们在Jenkins中添加这台slave机器。\n\n使用管理员帐号登录，\"Manage Jenkins\" -> \"Manage Nodes\" -> \"New Node\"，然后如下图所示配置:\n\n![Jenkins配置Slave机器](./jenkins-slave.png)\n\n主要的配置为:\n* Remote root directory 家目录/home/build\n* Usage 尽可能多使用(尽量不占用master进程)\n* Launch method 使用ssh模式\n * Host slave即上面启动的容器\n * Credentials 可以新增一个密码方式的验证build/build123\n* Availability 尽量让slave在线\n\n配置成功默认是离线的，稍等一会，会提示\"slave已经上线\"。\n\n## 持续集成第一步:迁出代码、编译\n\n本节开篇已经提到，持续集成的第一步即从代码仓库中迁出代码，我们来完成这项工作。\n\n首先在gerrit上准备一个项目，假设为lmsia-xyz，这是一个最简单的Spring Boot项目。\n\n为了能够提交、迁出代码，需要将公钥配置到gerrit上，点击右上角的名字 -> Setting -> SSH Public Keys，填入即可完成。\n\n准备好项目后，我们在Jenkins上新建一个\"Freestyle\"项目，命名为lmsia-xyz-build。\n\n首先配置代码仓库，如下图所示：\n\n![Jenkins配置Gerrit权限](./jenkins-gerrit.png)\n\n* 在\"Source Code Management\"中，选择\"Git\"，并填写gerrit的repo地址\n* Credentials中新增一个用户，为gerrit中的用户，要填写私钥\n\n此外，还要限制只能在slave上执行: Restrict where this project can be run中设置\"slave\"。\n\n完成后点击底部的Save。\n\n配置好后，我们执行第一次Build，在项目左侧菜单选择\"Build Now\"，可以在Log中查看输出如下:\n```\nBuilding remotely on slave in workspace /home/build/workspace/lmsia-xyz-build\nCloning the remote Git repository\nCloning repository ssh://lihy@10.1.64.72:29418/lmsia-xyz\n > git init /home/build/workspace/lmsia-xyz-build # timeout=10\nFetching upstream changes from ssh://lihy@10.1.64.72:29418/lmsia-xyz\n > git --version # timeout=10\nusing GIT_SSH to set credentials \n > git fetch --tags --progress ssh://lihy@10.1.64.72:29418/lmsia-xyz +refs/heads/*:refs/remotes/origin/*\n > git config remote.origin.url ssh://lihy@10.1.64.72:29418/lmsia-xyz # timeout=10\n > git config --add remote.origin.fetch +refs/heads/*:refs/remotes/origin/* # timeout=10\n > git config remote.origin.url ssh://lihy@10.1.64.72:29418/lmsia-xyz # timeout=10\nFetching upstream changes from ssh://lihy@10.1.64.72:29418/lmsia-xyz\nusing GIT_SSH to set credentials \n > git fetch --tags --progress ssh://lihy@10.1.64.72:29418/lmsia-xyz +refs/heads/*:refs/remotes/origin/*\n > git rev-parse refs/remotes/origin/master^{commit} # timeout=10\n > git rev-parse refs/remotes/origin/origin/master^{commit} # timeout=10\nChecking out Revision eab8a79ff6cde375c017b6f9eec29dff02a0bb85 (refs/remotes/origin/master)\n > git config core.sparsecheckout # timeout=10\n > git checkout -f eab8a79ff6cde375c017b6f9eec29dff02a0bb85\nCommit message: \"MOD: init commit\"\nFirst time build. Skipping changelog.\nFinished: SUCCESS\n```\n\n如上所示，我们成功地从代码仓库迁出了代码，第一步顺利完成！\n\n在迁出代码后，我们需要进行编译，回到lmsia-xyz-build项目的配置中，找到Build选项，新增一个\"Execute shell\"步骤，命令输入\"gradle build\"，点击底部\"Save\"。\n\n再次执行\"Build Now\"，发现项目依然执行成功，查看日志，可以发现编译也成功地执行了！\n\n至此，我们已经完成了代码的迁出和编译。\n\n## 打包镜像并上出到私有仓库\n\n在lmsia-xyz-build项目中新建一个Shell步骤，内容如下:\n```\n$HOME/ms2docker.sh\n```\n\n上述脚本需要添加到slave的镜像中，直接COPY即可，这里不再赘述。\n\n脚本内容如下：\n```shell\n#!/bin/bash\nset -e\n\n# Const\nDOCKER_REGISTRY=\"10.1.64.72\"\nPROJECT_VERSION=${BUILD_NUMBER:-1}\nPROJECT_NAME=$(basename `pwd`| sed -r 's/-build$//g')\nSERVER_NAME=\"$PROJECT_NAME-server\"\nJAR_NAME=\"$SERVER_NAME.jar\"\nDOCKER_FULLNAME=\"$PROJECT_NAME:$PROJECT_VERSION\"\n\n# Copy Jar\nfind . -name \"$SERVER_NAME*.jar\" -exec cp {} ./$JAR_NAME \\;\n\n# Generate Dockerfile\ncat > ./Dockerfile <<EOF\nFROM anapsix/alpine-java:8_server-jre\n\nVOLUME /tmp /app\nWORKDIR /app\nEXPOSE 8080 3000\nCOPY ${JAR_NAME} /app\nCMD [\"java\", \"-jar\", \"/app/${JAR_NAME}\"]\n\nEOF\n\n# Build\ndocker build .\ndocker build -t $PROJECT_NAME .\ndocker tag $PROJECT_NAME $DOCKER_REGISTRY/$DOCKER_FULLNAME\ndocker push $DOCKER_REGISTRY/$DOCKER_FULLNAME\n\n```\n\n简单解释一下:\n* 首先获得项目名称(根据当前执行的工作文件夹)\n* 查找生成的server.jar，并拷贝到当前目录\n* 编译一个Docker的镜像，包含Java环境和server的jar包\n* 上传Docker镜像到私有仓库\n\n在Jenkins配置好后，点击保存，重新打包。\n\n看一下结果:\n```\n....\nThe push refers to repository [10.1.64.72/lmsia-xyz]\nce4c6e84ae9a: Preparing\nc24b758e34d0: Preparing\nc28e906f67c9: Preparing\ncd7100a72410: Preparing\nc28e906f67c9: Layer already exists\nc24b758e34d0: Layer already exists\ncd7100a72410: Layer already exists\nce4c6e84ae9a: Pushed\n6: digest: sha256:86ccfb07945bdaf61c90470e3302774cde31d41b6c1a26647ea92fdb681536b3 size: 1158\nFinished: SUCCESS\n```\n\n如上所述，已经成功地打好Docker镜像并上传到私有仓库中。\n\n至此，我们经完成了持续集成的所有步骤，它包含:\n* 从gerrit上迁出代码到本地(slave机器)\n* 调用gradle编译\n* 使用Docker打镜像并上传到私有仓库\n\n## 拓展与思考\n1. 在开发过程中，微服务会频繁的打包、持续集成。这会产生大量历史镜像文件，这些历史版本并不会被使用，却浪费了大量的磁盘空间。请自行查找资料，实现\"只保留最近3个最新项目镜像\"这一功能。\n1. 本节的\"持续集成\"，主要指项目的编译部分。然而，在实际项目中，还需要在集成阶段进行单元测试、集成测试。如何在\"持续集成\"阶段完成测试工作，请结合实际情况思考这一问题。\n"
  },
  {
    "path": "legacy/ms-discovery/README.md",
    "content": "# 微服务的自动发现与负载均衡 \n\n采用微服务架构后，巨无霸服务被拆分为若干逻辑独立的微服务，导致服务数量逐渐上升。此外，为了保证系统的高可用和高性能，每一个微服务都会运行若干副本，这更进一步地导致微服务运行实例数量的攀升。\n\n面对数量逐渐攀升的微服务实例，我们不可能将服务的IP和端口都写在配置文件中，这种方案会使得系统无法维护。因此，采用微服务架构后，微服务服务（及实例）的自动发现是首先要解决的问题。\n\n前面已经提到，出于高可用和高性能考量，微服务往往同时部署多个副本。如何实现自动负载均衡呢？这是本章要讨论的第二个重点。\n\n本章将从容器技术谈起，介绍Docker和Kubernetes的优势。随后，我们会通过几个小例子，让大家快速上手Kubernetes。最后，我们结合一个微服务的实例，探讨如何利用Kubernetes的Service实现服务的自动发现与负载均衡。\n"
  },
  {
    "path": "legacy/ms-discovery/msd.md",
    "content": "# 微服务的自动发现\n\n在熟悉了的基本操作后，我们来讨论下如何实现微服务的自动发现。\n\nService是在Pod基础上做的另一层抽象，通过虚拟IP的方式，提供了统一的代理入口和负载均衡。\n\nService本身不会创建Pod，而是通过标签的方式与已有Pod产生关联，这与Deployment是类似的。因此，在创建第一个Service前，我们需要先应用之前的lmsia-abc-server-deployment，具体可参考前一节[Kubernetes 快速入门](kus-intro.md)\n\n下面来看一下Service描述文件，lmsia-abc-server-service.yaml\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: lmsia-abc-server-service\nspec:\n  selector:\n    app: lmsia-abc-server \n  ports:\n  - name: http\n    protocol: TCP\n    port: 8080\n  - name: rpc \n    protocol: TCP\n    port: 3000\n```\n\n与Deployment相比，上述Service的描述文件更简单一些。\n * kind: 类型是Service\n * metadata.name: 定义了Service名字\n * spec.selector.app: 定义了要关联的Pod标签\n * spec.ports: 定义了需要进行负载均衡的端口，这里定义了两套需要负载均衡的端口，http的8080和rpc的3000。\n\n有了描述文件后，我们来应用服务：\n```shell\nkubectl apply -f lmsia-abc-server-service.yaml\n\nservice \"lmsia-abc-server-service\" created\n```\n\n成功创建Service后，可以使用'describe service'来查看：\n```\nkubectl describe service lmsia-abc-server-service\n\nName:              lmsia-abc-server-service\nNamespace:         default\nLabels:            <none>\nAnnotations:       kubectl.kubernetes.io/last-applied-configuration={\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"name\":\"lmsia-abc-server-service\",\"namespace\":\"default\"},\"spec\":{\"ports\":[{\"name\":\"htt...\nSelector:          app=lmsia-abc-server\nType:              ClusterIP\nIP:                10.109.20.138\nPort:              http  8080/TCP\nTargetPort:        8080/TCP\nEndpoints:         172.17.0.4:8080,172.17.0.5:8080\nSession Affinity:  None\nEvents:            <none>\n\n```\n\n上面返回的结果中，有一些关键信息：\n * Type: 指的是ServiceType，ClusterIP是仅供集群内访问的负载均衡IP。类似的，如果想将虚拟IP暴露给集群外，可以使用NodePort等，具体可以参考官方文档[Publising Service Types](https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types)。\n * IP: 服务提同的虚拟IP地址。\n * Port: 微服务进程上的端口，即HTTP的8080和RPC的3000\n * TargetPort: 虚拟IP对外提供负载均衡的端口，由于我们未单独制定，默认是与上述Port保持一致的。\n * Endpoints：我们在Deployment中定义的两个Pod。Service通过虚拟IP将流量分发到这两个后端Pod上。\n\n让我们来验证下负载均衡的配置是否生效，由于rpc接口的数据格式较为复杂，在此我们仅验证http端口。\n\n首先登录到minikube\n```shell\nminikube ssh\n\ncurl http://10.109.20.138:8080/lmsia-abc/api/\nHello, REST\n\ncurl http://10.109.20.138:8080/lmsia-abc/api/\nHello, REST\n\n```\n\n我们执行了两次，都成功了，那么这个请求真的被均匀地分发到后端的进程上了么？我们需要验证一下。\n\n首先获取两个容器的ID\n```shell\n\n# list pod\nkubectl get pods -l app=lmsia-abc-server\nNAME                                          READY     STATUS    RESTARTS   AGE\nlmsia-abc-server-deployment-bd4949ff9-7bgvq   1/1       Running   0          16m\nlmsia-abc-server-deployment-bd4949ff9-mlmlq   1/1       Running   0          16m\n\n# get container id for pod1\nkubectl describe pod lmsia-abc-server-deployment-bd4949ff9-7bgvq\n...\nName:           lmsia-abc-server-deployment-bd4949ff9-7bgvq\n...\nContainers:\n  lmsia-abc-server-ct:\n    Container ID:   docker://a146ee545d11638a331d1696e7e6e3c88cc3231b97f3eb50c63cb9f50724cf2c\n...\n\n# get container id for pod 2\nkubectl describe pod\n...\nName:           lmsia-abc-server-deployment-bd4949ff9-mlmlq\n...\nContainers:\n  lmsia-abc-server-ct:\n    Container ID:   docker://608decbb198dcbdce5442a4401eeeec1cb316e483ddba2d5c993ea10081a5e6a\n...\n\n```\n\n登录minikube集群，分别查看两个Container的日志\n```shell\nminikube ssh\n\n# check pod 1 access log\n$ docker exec -i -t a146ee545d11638a331d1696e7e6e3c88cc3231b97f3eb50c63cb9f50724cf2c cat /app/logs/access_log.2018-05-14.log\n10.0.2.15 - - [14/May/2018:07:27:57 +0000] \"GET /lmsia-abc/api/ HTTP/1.1\" 200 11\n\n# check pod 2 access log\n$ docker exec -i -t 608decbb198dcbdce5442a4401eeeec1cb316e483ddba2d5c993ea10081a5e6a cat /app/logs/access_log.2018-05-14.log\n10.0.2.15 - - [14/May/2018:07:27:56 +0000] \"GET /lmsia-abc/api/ HTTP/1.1\" 200 11\n```\n\n这里需要说明下'docker exec -i -t'，是针对Docker容器执行命令，要执行的命令即后面的cat /app/logs....\n\n查看了两个Pod对应的Container的日志，可以发现：虽然我们的curl是访问的虚拟IP，但是流量被均衡地分发到了2个后端容器上。至此，我们已经通过Service实现了多节点的自动负载均衡。\n\n需要指出的是：Kubernetes的虚拟IP内置了多种实现，目前以ipvs性能最好，具体可以查看[Virtual IPs and service proxies](https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies)\n\n现在让我们来回顾下这一节的标题：\"微服务的自动发现\"。对于服务发现这个需求，我们目前的效果似乎并不这么完美，为什么这样说呢？我们目前是通过虚拟IP直接访问的服务，但在实际生产环境中，每个Service创建的虚拟IP并不固定，我们不可能将这些虚拟IP分别配置在依赖的众多微服务中。\n\n幸运的是，Kubernetes早就为我们解决了这个问题。在创建Service的同时，Kubernetes还为我们创建了一条DNS记录，我们可以通过域名直接访问虚拟IP：\n```shell\ndocker exec -i -t 608decbb198dcbdce5442a4401eeeec1cb316e483ddba2d5c993ea10081a5e6a busybox wget -q -O - http://lmsia-abc-server-service:8080/lmsia-abc/api/\n\nHello, REST\n```\n\n如上所示，通过lmsia-abc-server-service这个域名，就可以成功地访问虚拟IP了。对于ClusterIP的Service，域名的默认组成是'服务名.服务所在命名空间.svc.cluster.集群域名'，或者简单使用`服务名`[^1]，上面例子中我们采用的就是后者。\n\n让我们用一张图来回顾下服务发现、负载均衡流程：\n\n![基于Kubernetes的服务发现与负载均衡](./service-discovery.png \"基于Kubernetes的服务发现与负载均衡\")\n\n如上图所示：\n1. 约定好微服务Service的命名方式\n1. 通过DNS服务获取微服务Service对应的虚拟IP(VIP)\n1. 访问VIP和端口(3000)\n1. Kubernetes的VIP自动完成了负载均衡，转发到后端Service B的3个节点(Pod/Docker)上\n\n至此，我们借助Kubernetes的Service功能，\"近似完美\"地实现了服务的注册与发现。\n\n为什么讲\"近似完美\"呢？这里还会有一个小坑。熟悉DNS协议的朋友知道，为了提升查询效率，DNS被设计成可以多级缓存的。在Java的JVM虚拟机上，也会进行DNS缓存，但这个缓存有效期默认是-1即永久。这也就意味着，如果我们删除这个Service重新创建，那么虚拟IP的变更将不会自动反馈到相应微服务的JVM中。\n\n为了解决这个小坑，一般建议修改JVM的安全设置，修改缓存TTL时间，具体可以参考[亚马逊AWS的这篇介绍](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/java-dg-jvm-ttl.html)。\n\n我们为本章构建的Docker镜像也自动解决了这个问题：\n\n```shell\nFROM anapsix/alpine-java:8_server-jre\n\nWORKDIR /app\n\nRUN mkdir -p /app/logs\n\nADD lmsia-abc-server.jar /app\n\nCMD [\"java\", \"-jar\", \"lmsia-abc-server.jar\"]\n\n```\n\n其中`anapsix/alpine-java:8_server-jre`是我们依赖的基础镜像，它将DNS Cache设置为了10秒钟，读者也可以直接使用这个基础镜像。\n\n需要特别说明的时：若想使用上述的自动发现机制，必须使用Kubernetes的DNS服务，它默认是开启的：\n```shell\nkubectl -n kube-system get svc kube-dns\n\nNAME       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)         AGE\nkube-dns   ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP   3d\n```\n\n通过Kubernetes创建的Pod(Docker)，已经自动配置了上述DNS。若想在在集群外使用这个DNS，有两种方案：\n* 将DNS通过NodePort的方式暴露出去，可以参考[这篇讨论](https://stackoverflow.com/questions/37449121/how-to-expose-kube-dns-service-for-queries-outside-cluster)\n* 打通办公内网和集群内网，本书后续章节[OpenVPN + NAT 打通办公网与IDC](devops/openvpn-nat.md)将对此做出介绍。\n\n[^1]: 这一特性并未记录在官方文档中，本书假设该特性持续有效。\n"
  },
  {
    "path": "legacy/ms-discovery/service-discovery.xml",
    "content": "<mxfile userAgent=\"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36\" version=\"8.6.6\" editor=\"www.draw.io\" type=\"device\"><diagram id=\"a897b486-16f4-0dce-ec20-64fbc7c553e4\" name=\"Page-1\">5Vlbk5owFP41PraDBBEf1b11uu3srDPt9qkT4QjpBkJj8NJf30QSAcFdO0WdUV9MziUk5/vOyWHooHG8uuc4jb6wAGjHtoJVB910bLvr2K78U5J1Lun3tCDkJNBGhWBC/oAWWlqakQDmFUPBGBUkrQp9liTgi4oMc86WVbMZo9WnpjiEmmDiY1qXfieBiHKp17MK+QOQMDJP7lpaM8X+a8hZlujndWw02/xydYzNWtp+HuGALUsidNtBY86YyEfxagxUxdaELfe726Pd7ptDIg5y0B5zsTZnh0CGQk8ZFxELWYLpbSEdbc4HagVLziIRUznsyiGsiHgpjX8ok489NUsEX79oj82k0P0CIdaaAzgTTIqK5z4yluoV62czm2cZ9/XukWYL5iFoq34uUucquel43AOLQW5HGnCgWJBFlQJYMync2hXRlAMd0D3BtS8/uN7ZgosuP7iDcwVX72WBaQamkLlU7mo0Y/II5ai7vzNmFB/mm1gMpUHXTVeFUo5C9f85mwJPQMjargpTvuSUG/Xz0/jnt09PUnfHlBDHMoKjZDpP8zV2zHctJsAXRIbTtrbG8qD5jo3HDmWqhFhGRMAkxRtIlvJqq5JkRigdMyq3pnxRgMGb+VI+F5y9Qknj+h5MZ28BvwAuYPUmqFqLBk7usjaXq746lsXF1DXXSVS6lDzr/3lgt3s9nCqVnHoqmQidPpecI+VSwfbhoWyX3UaqhllMh75QbB0pIhLZ+DziKdAnNieCsESaTJkQLJYGVClG27amxHLd2BRrDCkJla9gO4nDMkFJIjPEdGtWO7nhoWpq9OqpgRoyw20hM/rHRbWh2j1DSmWUledevOsVcm+96/57veuBFzhN9c6zp8h128G0b3UroPYPLHdtgOqdEVT7mkBtytSjoTo4I6romlC1kX06VO1Le7kygSp3LehcTYvZzKnfAG6+Thr7/Atp5rsDt1oHT9rMt/BW5zRh+sDmIsEx7EfuEMl76MoQiyqEVagSlsAOrlqEdV/qS+Cg3PSahjUmQbCpD02cqbKqDRKYMmlI4A5qJHAaOGC3wYEW3kYaOaDe2t/HGllWQ9JfHQMQ2mmHXO/joPxzTkcI90iEeGQ4aCzxdYmEByc+XB0Nesg9Ew3ktPjGsNGVPuSg278=</diagram></mxfile>"
  },
  {
    "path": "legacy/ms-log/README.md",
    "content": "# 微服务日志监控\n\n与前端或移动端产品不同，微服务运行于后台，我们不能直观的观察到服务端的运行状况。因此，合理地记录日志是检查服务端运行状态，查找问题的有效手段。\n\n服务端日志的常见用途有：\n* 业务日志：对一些分支条件进行简单记录，方便日后排查。\n* 异常记录：记录运行时异常或业务逻辑异常，方便事后排查。\n* 性能定位：日志系统一般自带时间戳，可以通过此方法排查函数调用的时间消耗。\n\n对于微服务架构，记录下来日志只是第一步，如何使用日志是更大的难点。\n\n试想在微服务的分布式系统下，有多种微服务，每个微服务又存在多个副本，日志文件可能散落在几百个不同的路径下。\n\n这种情况下，面临着如下挑战：\n* 如何快速找到某个微服务的所有（或某个）副本的日志？\n* 微服务之间存在调用，如何从一次完整的调用角度，来分析相关服务的日志？\n* 如何对异常日志进行预警？\n\n上述挑战实际对应了日志监控的三个问题，即\n* 微服务日志的收集、管理与查询\n* 微服务的调用链跟踪\n* 微服务异常日志的预警\n\n本章将从微服务的日志系统展开讨论，探讨了使用Logback记录微服务日志的相关问题。\n\n接着，我们讨论如何实现调用链的跟踪，并引入了TraceId类库。\n\n最后，讨论如何使用\"EBLK架构\"对微服务的日志进行收集、管理、查询。\n\n"
  },
  {
    "path": "legacy/ms-log/elk-devops.md",
    "content": "# ELK日志分析平台的运维\n\n在上一节中，我们在日志文件中增加了调用链信息，方便我们追踪每一次调用的完整关系链条。\n\n尽管有了追踪信息，可以更好地排查信息。但在微服务架构下，微服务众多，每个微服务又会启动若干个副本，日志文件的数量会随着文件系统迅速增加。\n\n为了排查一个问题，我们可能要分别到十几个服务上打开几十个不同的文件，效率非常低下。\n\nELK就是在这种场景下营运而生的，ELK是一套数据分析套件，由Elasticsearch, Logstach, Kibana组成。在微服务架构的应用场景下，一般用来分析日志。\n\n在ELK套件中：\n* Logstash负责从不同的微服务、不同的副本上收集日志文件，进行格式化。\n* Elasticsearch负责日志数据的存储、索引\n* Kibana提供了友好的数据可视化、分析界面\n\n![ELK套件流程图](./elk-stack.jpg \"ELK套件流程图\")\n\n在本节中，我们暂不接入微服务的日志，单纯探讨ELK套件的运维工作。\n\n与之前类似，我们的ELK套件将运行在Kubernetes集群上。\n\n## Elasticsearch的运维\n\nElasticsearch是ELK套件的核心与中枢。我们首先来看一下它的运维工作。\n\nElasticsearch的索引需要持久化存储，我们首先声明Pv:\n\n```yaml\n\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: pv031 \nspec:\n  storageClassName: standard\n  accessModes:\n    - ReadWriteOnce\n  capacity:\n    storage: 20Gi\n  hostPath:\n    path: /data/pv031/\n\n```\n\n然后创建一下这个pv：\n```shell\n\nkubectl apply -f ./pvs.yaml\n\n```\n\n下面看一下elasticserch的定义:\n\n```yaml\n\napiVersion: v1\nkind: Service\nmetadata:\n  name: es\nspec:\n  ports:\n  - name: p2\n    port: 9200\n  - name: p3\n    port: 9300\n  selector:\n    app: elasticsearch\n  clusterIP: None\n---\napiVersion: apps/v1\nkind: StatefulSet \nmetadata:\n  name: elasticsearch\nspec:\n  selector:\n    matchLabels:\n      app: elasticsearch\n  serviceName: \"es\"\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        app: elasticsearch \n    spec:\n      hostname: elasticsearch\n      containers:\n      - name: elasticsearch-ct\n        image: docker.elastic.co/elasticsearch/elasticsearch:6.3.1 \n        ports:\n        - containerPort: 9200 \n        - containerPort: 9300\n        env:\n        - name: \"ES_JAVA_OPTS\"\n          value: \"-Xms384m -Xmx384m\"\n        volumeMounts:\n        - mountPath: /usr/share/elasticsearch/data \n          name: elasticsearch-pvc\n  volumeClaimTemplates:\n  - metadata:\n      name: elasticsearch-pvc\n    spec:\n      storageClassName: standard\n      accessModes:\n        - ReadWriteOnce\n      resources:\n        requests:\n          storage: 20Gi\n\n```\n\n如上所述：\n* 考虑到日志数据量大了之后，可能需要分片，我们这里采用了StatefulSet，但目前只有一台服务器。\n* 暴露两个端口9200和9300，前者是Restful接口，后者是集群同步接口\n* 采用IP直发，service伪组名是\"es\"。这样配置后，所有Pod都可以通过elasticsearch-0.es来直接访问这台服务器\n\n启动一下：\n```yaml\nkubctl apply -f ./elasticsearch.yaml\n```\n\n如果启动失败，可以查看日志，可能是如下原因：\n```\nkubectl logs elasticsearch-0\n\n...\nvm.max_map_count < 262144\n...\n\n```\n\n这种情况，可以使用具有sudo权限的帐号，更改宿主机（物理机）的配置：\n```yaml\nsudo sysctl -w vm.max_map_count=262144\n```\n\n再次启动一下，可以发现启动成功：\n```yaml\nNAME                                                READY     STATUS    RESTARTS   AGE\nelasticsearch-0                                     1/1       Running   4          6h\n```\n\n## Logstash运维\n\n在启动了Elasticsearch后，我们来看一下Logstash的运维。\n\n前面已经提到了，我们本节先不会接入Spring Boot的日志，为了方便演示，我们先Mock一个定时任务，每间隔5秒生成日志：\n```yaml\napiVersion: v1\ndata:\n  logstash.yml: |\n    http.host: \"0.0.0.0\"\n    xpack.monitoring.elasticsearch.url: http://elasticsearch:9200\n    input {\n      heartbeat {\n        interval => 5\n        message  => 'Hello from Logstash 💓'\n      }\n    }\n    \n    output {\n      elasticsearch {\n        hosts    => [ 'elasticsearch-0.es' ]\n        user     => 'elastic'\n        password => ''\n      }\n    }\n\nkind: ConfigMap\nmetadata:\n  name: logstash-configmap\n\n```\n\n上述是一个ConfigMap，我们在本书中是第一次使用它。它相当于一个可以加载的Volume，可以方便的直接追加到Pod上。\n\n来创建这个ConfigMap:\n```yaml\nkubectl apply -f logstash-configmap.yaml\n```\n\n下面看一下Logstash的部署：\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: ls\nspec:\n  ports:\n  - name: p\n    port: 5000\n  selector:\n    app: logstash\n  clusterIP: None\n---\napiVersion: apps/v1\nkind: StatefulSet \nmetadata:\n  name: logstash\nspec:\n  selector:\n    matchLabels:\n      app: logstash\n  serviceName: \"ls\"\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        app: logstash\n    spec:\n      hostname: logstash\n      containers:\n      - name: logstash-ct\n        image: docker.elastic.co/logstash/logstash:6.3.1 \n        ports:\n        - containerPort: 5000 \n        env:\n        - name: \"ES_JAVA_OPTS\"\n          value: \"-Xms384m -Xmx384m\"\n        - name: \"XPACK_MONITORING_ENABLED\"\n          value: \"false\"\n        - name: \"XPACK_MONITORING_ELASTICSEARCH_URL\"\n          value: \"http://elasticsearch-0.es:9200\"\n        volumeMounts:\n        - name: logstash-configmap\n          mountPath: /usr/share/logstash/pipeline/logstash.conf\n          subPath: logstash.conf\n      volumes:\n      - name: logstash-configmap\n        configMap:\n          name: logstash-configmap\n\n```\n\n如上所述：\n* 我们使用了刚才配置的logstash-configmap，并覆盖到Pod的/usr/share/logstash/pipeline/logstash.conf，这个文件中\n* 监控地址是elasticsearch-0.es:9200，即前面启动的es服务地址\n\n启动一下：\n```shell\nkubectl apply -f ./logstash.yaml\n```\n\n稍等一会，启动成功：\n```shell\n\nNAME                                                READY     STATUS    RESTARTS   AGE\nlogstash-0                                          1/1       Running   0          7h\n\n```\n\n## Kibana的运维\n\n最后，我们来看一下Kibana的运维：\n```yaml\n\napiVersion: v1\nkind: Service\nmetadata:\n  name: kb\nspec:\n  selector:\n    app: kibana \n  clusterIP: None\n---\napiVersion: apps/v1\nkind: Deployment \nmetadata:\n  name: kibana \nspec:\n  selector:\n    matchLabels:\n      app: kibana \n  replicas: 1\n  template:\n    metadata:\n      labels:\n        app: kibana \n    spec:\n      hostname: kibana\n      containers:\n      - name: kibana-ct\n        image: docker.elastic.co/kibana/kibana:6.3.1 \n        ports:\n        - containerPort: 5601\n          hostPort: 5601\n        env:\n        - name: \"ES_JAVA_OPTS\"\n          value: \"-Xms384m -Xmx384m\"\n        - name: \"ELASTICSEARCH_URL\"\n          value: \"http://elasticsearch-0.es:9200\"\n        - name: \"XPACK_MONITORING_ENABLED\"\n          value: \"false\"\n```\n\n一般来说，Kibana作为前端展示组件，只需要一台就够了，我们直接用了Deployment。\n\n尝试打开浏览器访问一下：\n\n![Kibana界面图](./kibana-chrome.png \"Kibana界面图\")\n\n如果一切顺利，可以发现，访问成功。\n\n对于新一些的ElasticSearch/Kibana版本，可能需要先配置一下索引，比较简单，跟着向导就可以完成。\n\nKibana的功能非常强大，拿来做日志分析实际有点大材小用。感兴趣的话，可以参考[官方使用教程](https://www.elastic.co/guide/en/kibana/current/getting-started.html)。\n\n我们对ELK的运维就介绍到这里。\n"
  },
  {
    "path": "legacy/ms-log/sb-eblk.md",
    "content": "# Spring Boot整合EBLK日志分析平台\n\n不知道你有没有注意到，这一节的标题是\"EBLK日志分析平台\"，而上一节的标题中是\"ELK日志分析平台\"。是的，你没有看错，这也不是笔误。\n\n在ELK平台中，Logstash负责收集微服务的各种Log文件，发送给ElasticSearch。\n\n当微服务数量少、副本数也不多的时候，Logstash是可以胜任的。随着微服务数量不断增多，副本数不断增长，Logstash的负载会越来越高，极易造成单点故障。\n\n此外，在我们的微服务架构下，各个微服务进程是运行在Kubernetes集群上的，它们的日志文件可能分散在各个物理机上。如何让单一的Logstash收集这些遍布各处的日志文件，也是一个难题。\n\n一个简单的想法就是启用边车模式，即每个微服务启动时，同时伴随部署一个Logstash，这样就可以解决单点故障和收集的问题。\n\n想法是好的，但Logstash本身的结构较为复杂，同时具有监听文件、网络、批处理等各种复杂功能，此外Logstash需要JVM运行环境，内存占用较大。\n\n为了更加轻量级级的收集日志，ElasticSearch推出了Beat，我们以边车模式伴随微服务进行部署。关于Beat与Logstash的对比，可以参考[这篇文章](https://logz.io/blog/filebeat-vs-logstash/)。\n\nBeat负责收集日志，并将日志发送给Logstash。这样看起来还是没有解决Logstash的单点故障？\n\n是的，但经过Beat转发后，我们实际上可以配置多个Logstah结点从而解决掉单点故障。\n\n此外，Beat可以缓存日志，当Logstash挂掉后，会自动重试。Logstash恢复后，可以继续处理日志的发送。加上B这一层后，整EBLK的架构如下所示：\n\n![EBLK架构图](./eblk.png \"EBLK架构图\")\n\n在本小节的前半部分，我们将在一个受限环境中，使用Beat收集日志，并发送给Logstash。后半部分，将讨论如何在Kubernetes中应用便车模式，让Beat伴随微服务一同启动。\n\n## 使用Beat收集日志\n\nBeats是一系列日志收集工具的统称，官方推出了多种Beat，如：Filebeat, Metricbeat, Packetbeat, Winlogbeat等等，详细可以参见[官方介绍](https://www.elastic.co/products/beats)。\n\n在我们的场景下，需要解析微服务输出的日志文件，直接用Filebeat即可。\n\n首先来看一下FileBeat的配置:\n\n```yaml\n\napiVersion: v1\ndata:\n  filebeat.yml: |\n    filebeat.inputs:\n    - type: log\n      enabled: true\n      multiline.pattern: '^2'\n      multiline.negate: true\n      multiline.match: after\n      name: filebeat-test\n      paths:\n        - /usr/share/filebeat/*.log\n    output.logstash:\n      hosts: [\"logstash-0.ls:5555\"]\n\nkind: ConfigMap\nmetadata:\n  name: filebeat-configmap\n\n```\n\n上述配置包含2个部分:\n* 输入监听/user/share/filebeat/下后缀为log的文件，这里只是限定环境下的测试，并非线上微服务的日志，支持多行自动合并为同一个事件（主要是异常时调用堆栈信息）。\n* 输出到logstash, logstash-0.ls:5555\n\n然后我们看一下FileBeat的服务定义：\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: ls\nspec:\n  ports:\n  - name: p\n    port: 5000\n  selector:\n    app: filebeat\n  clusterIP: None\n---\napiVersion: apps/v1\nkind: StatefulSet \nmetadata:\n  name: filebeat\nspec:\n  selector:\n    matchLabels:\n      app: filebeat\n  serviceName: \"ls\"\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        app: filebeat\n    spec:\n      hostname: filebeat\n      containers:\n      - name: filebeat-ct\n        image: docker.elastic.co/beats/filebeat:6.3.2 \n        env:\n        - name: \"ES_JAVA_OPTS\"\n          value: \"-Xms384m -Xmx384m\"\n        - name: \"XPACK_MONITORING_ENABLED\"\n          value: \"false\"\n        - name: \"XPACK_MONITORING_ELASTICSEARCH_URL\"\n          value: \"http://elasticsearch-0.es:9200\"\n        volumeMounts:\n        - name: filebeat-configmap\n          mountPath: /usr/share/filebeat/filebeat.yml\n          subPath: filebeat.yml\n      volumes:\n      - name: filebeat-configmap\n        configMap:\n          name: filebeat-configmap\n\n```\n\n如上所示，配置基本与之前的Logstash相同，并且加载了刚配置好的filebeat.yml。\n\n## Logstash汇总日志\n\n对应地，logstash也需要做对应的调整：\n```yaml\n\napiVersion: v1\ndata:\n  logstash.conf: |\n\n    input {\n      beats {\n        port => 5555\n      }\n    }\n\n    filter {\n      grok {\n        match => {\"message\" => \"(?m)^(?<TIMESTAMP>%{YEAR}-%{MONTHNUM}-%{MONTHDAY} %{TIME}) \\[%{LOGLEVEL:LEVEL}\\] \\[(?<THREAD>.*?)\\] \\[(?<LOGGER>.*?)\\] \\[tr=(?<TRACE_ID>.*?)\\]\\s+(?<MSG>.*)\" }\n      }\n    }\n    \n    output {\n      elasticsearch {\n        hosts    => [ 'elasticsearch-0.es' ]\n        user     => 'elastic'\n        password => ''\n      }\n    }\n\nkind: ConfigMap\nmetadata:\n  name: logstash-configmap\n\n```\n\n如上所示，我们修改了logstash的配置：\n* input是beat格式，端口5555，与上面filebeat的配置对应\n* filter对输入的beat事件进行解析。这里使用了grok插件，具体的语法可以参考[官方grok插件介绍](https://www.elastic.co/guide/en/logstash/current/plugins-filters-grok.html)\n* output输出到elasticsearch，这里没有变化\n\n我们重启Logstash和FileBeat后，尝试向FileBeat的Docker中写入几行日志，稍等几秒，打开Kibana，可以发现，日志已经可以检索到了。\n\n![接入了FileBeat后的Kibana](./kibana-filebeat.png \"接入了FileBeat后的Kibana\")\n\n## 将FileBeat与Spring Boot进行整合\n\n前面已经提到，微服务数量、副本数众多、遍布在集群的各个物理机上，日志收集、汇总起来非常麻烦，所以一般来说，需要使用边车模式，即一个微服务伴随一个日志收集器(FileBeat)。\n\n上述模式的实现，有两个技术选择：\n* 使用Kubernetes的Pod多容器模式\n* 手动将FileBeat打包进微服务的镜像内。\n\n方案二比较传统，也易于理解，可以参考[这篇文章]（https://stackoverflow.com/questions/47811121/dockerfile-springboot-app-with-filebeat）\n\n而方案一，则是利用了Kubernetes的原生支持特性。\n\nKubernets中的最小操作单位是Pod，Pod中可以启动多个Docker容器，且同他们之间共享同样的磁盘、端口。\n\n关于微服务的服务定义、FileBeat定义，我们前面已经分别介绍过了，所需要做的，就是将他们“粘贴”到同一个Pod里面。\n\n这里，我不再赘述具体描述，而是作为一个思考题留给你来实现。如果实现起来有困难，可以参考[Multi-Container Pods in Kubernetes]（https://linchpiner.github.io/k8s-multi-container-pods.html）。\n"
  },
  {
    "path": "legacy/ms-log/sb-logback.md",
    "content": "# Spring Boot配置Logback及HTTP日志\n\n系统上先后，需要进行一系列的运维、监控工作，可能还需要排查业务故障和系统问题。\n\n服务已经上线了，不能像本地开发的一样“”打断点调试“，此时，日志的作用就非常重要了。\n\n与其他服务框架类似，Spring Boot也默认集成了日志系统，默认采用Logback日志类库。\n\nLogback是Log4j的作者开发的另一款日志类库，与其他同类竞品相比，它的优势有：\n* 更高的性能，官方说比Log4j快10倍以上\n* 原生兼容slf4j\n* 支持多环境配置、自动切换、压缩等高级功能\n\n在本节的前半部分，我们将讨论如何在Spring Boot中如何使用Logback。本节的后半部分，我们看一下如何在Spring Boot中启用HTTP访问日志(内嵌的Tomcat日志)。\n\n## Spring Boot中配置Logback\n\n在Spring Boot中配置Logback只需要两步：\n* 确认类路中含有logback，这一般是通过其他starter自动带上的，例如spring-boot-starter-web\n* 定义配置文件:logback-spring.xml\n\n我们来看一下配置好的文件:\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n\n    <!-- For console -->\n    <appender name=\"ConsoleAppender\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <charset>UTF-8</charset>\n            <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] [%thread] [%logger] [tr=%mdc{TRACE_ID:-0}] %msg %n</Pattern>\n        </encoder>\n    </appender>\n\n    <!-- For file with daily rollover -->\n    <appender name=\"ServerFileAppender\" class=\"ch.qos.logback.core.rolling.RollingFileAppender\">\n        <encoder>\n            <charset>UTF-8</charset>\n            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] [%thread] [%logger] [tr=%X{TRACE_ID:-0}] %msg %n</pattern>\n        </encoder>\n\n        <file>/app/logs/lmsia-abc.log</file>\n\n        <rollingPolicy class=\"ch.qos.logback.core.rolling.TimeBasedRollingPolicy\">\n            <!-- daily rollover with gz -->\n            <fileNamePattern>/app/logs/lmsia-abc.%d{yyyy_MM_dd}.log.gz</fileNamePattern>\n            <!-- keep 30 days' max -->\n            <maxHistory>30</maxHistory>\n        </rollingPolicy>\n    </appender>\n\n    <!-- console only if local active -->\n    <springProfile name=\"local\">\n        <root level=\"INFO\">\n            <appender-ref ref=\"ConsoleAppender\"/>\n        </root>\n    </springProfile>\n    <!-- file only if test or online active -->\n    <springProfile name=\"test,online\">\n        <root level=\"INFO\">\n            <appender-ref ref=\"ServerFileAppender\"/>\n        </root>\n    </springProfile>\n\n</configuration>\n\n```\n\n如上所示，我们的配置中包含了2个Appender(可理解为两种日志输出方法)：\n* ConsoleAppender: 直接输出到命令行\n* ServerFileAppender: 输出到/app/logs/lmsia-abc.log文件中，并且:按天自动切换文件、并做gz压缩、最多保留30天。\n\n上述切换、压缩、30天仅通过几行就搞定了，可见logback的强大之处！\n\n在下面，我们通过对不同profile的判断，可以让不同的Appender生效。\n当我们在本地执行时，默认是local的profile，此时我们只运行ConsoleAppender，即直接输出到命令行，方便调试。\n当在服务器执行时，如测试环境test和生产环境online，我们只启用ServerFileAppender。因为此时没有人会看stdout的输出，都是通过看文件的方式来看日志的。\n\n最后，我们简单看一下日志格式，即Pattern:\n```\n%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] [%thread] [%logger] [tr=%X{TRACE_ID:-0}] %msg %n\n```\n\n几个部分分别表示:\n* 日期，如2018-06-07 18:30:23.124\n* 日志级别，INFO, ERROR等\n* 日志线程，如果有名字会优先用名字，没有用线程ID\n* Logger名字，一般是类名\n* TraceId，追踪信息，下一节将介绍它\n* 消息体及换行，如果有异常及异常栈，会自动输出在后面\n\n在代码中使用，也是非常简单：\n\n```\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nprivate Logger LOG = LoggerFactory.getLogger(getClass());\n\nLOG.info(\"Test\");\n```\n\n## 配置Tomcat日志\n\nSpring Boot默认内置了Tomcat服务器，从而实现了真正的\"开箱即用\"，如何开启Tomcat的HTTP访问日志呢？\n\n一般有两种方法:\n* 在yaml中配置\n* 通过代码实现\n\n其中yaml中配置的方案最简单，但每个项目都要配置一次，非常麻烦，网上资料很多，这里不做介绍了。\n\n我们重点看一下第二种方案，我们可以将它抽成一个包，别的项目引用这个包时候，自动启用HTTP访问日志。\n\n```java\nimport org.apache.catalina.valves.AccessLogValve;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;\nimport org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;\nimport org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;\nimport org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;\n\n@Configuration\n@ConditionalOnWebApplication\npublic class TomcatAccessLogConfiguration\n        extends WebMvcConfigurerAdapter implements EmbeddedServletContainerCustomizer {\n\n    private Logger LOG = LoggerFactory.getLogger(getClass());\n\n    @Override\n    public void customize(ConfigurableEmbeddedServletContainer container) {\n        if (container instanceof TomcatEmbeddedServletContainerFactory) {\n            TomcatEmbeddedServletContainerFactory factory = (TomcatEmbeddedServletContainerFactory) container;\n            AccessLogValve accessLogValve = new AccessLogValve();\n            accessLogValve.setEnabled(true);\n            accessLogValve.setDirectory(\"/app/logs/\");\n            accessLogValve.setPattern(\"common\");\n            accessLogValve.setSuffix(\".log\");\n            factory.addContextValves(accessLogValve);\n        } else {\n            LOG.error(\"This customizer does not support your configured container!\");\n        }\n    }\n}\n\n```\n\n如上所示:\n* 当启用Web时，自动激活这个自动配置\n* 使用默认的日志格式common\n* 路径在/app/logs下，后缀是.log\n\n当然不要忘记加一个spring.factories\n```\norg.springframework.boot.autoconfigure.EnableAutoConfiguration=\\\ncom.coder4.lmsia.commons.http.configuration.TomcatAccessLogConfiguration\n```\n\n这样，当别的Spring Boot项目引用这个库时，就会自动启用HTTP日志了。\n\n这个HTTP日志也是支持按天滚动，只不过不支持压缩，如果你想对其进行更多定制，推荐直接阅读Tomcat的相关源代码。\n"
  },
  {
    "path": "legacy/ms-log/sb-trace.md",
    "content": "# Spring Boot整合分布式调用链追踪\n\n在上一节，我们讨论了如何在Spring Boot项目中配置LogBack日志系统。\n\n如果是传统的巨服务架构，有日志就能够满足基本的需求了。\n\n但面对微服务，事情变得有一些复杂：\n* 微服务之间存在复杂的调用链路，例如A -> B -> C\n* 为了高可用，每个微服务可能存在多个实例\n\n设想我们有A, B, C三个微服务，每个微服务有2个实例，在调用链A -> B -> C的过程中，发生了异常，导致某个请求挂掉了。\n\n此时，我们已经有日志系统了，该如何检查呢？我们需要一次检查2个A服务，如果运气不好的话，可能没有异常，我们接下来检查B服务，也可能没有异常，最后检查C服务，发现了异常。\n\n在上述任务排查过程中不难看出，在微服务架构下，各个服务的相互调用非常复杂。\n\n实际上，我们可以引入调用链的追踪机制，来查明这种关系。\n\n调用链追踪是这样一直机制：对于每一次调用，例如从A开始，就生成一条\"调用链路\"并赋一个追踪信息（后简称TraceId），调用到B时，会继承这个TraceId，如果它又调用了C服务，这个TraceId也会传递下去，直到调用链的末端。若是另一次调用链条，则会使用另一个随机生成的TraceId。\n\n针对这种追踪机制，业界已经存在了一些较为成熟的方案，例如[Zipkin](https://zipkin.io/)能够很好的完成链路调用的追踪工作。\n\n如果你使用的是Spring Boot全家桶，那么Zipkin可以较为方便地集成进来，可以参考[这篇教程](https://spring.io/blog/2016/02/15/distributed-tracing-with-spring-cloud-sleuth-and-spring-cloud-zipkin)。\n\n本书将选择一种更为直接的方式：手写代码实现调用追踪，并将它整合进日志系统中。\n\n这样做的好处有：\n* 如果你用过Zipkin，就能发现，它并不能覆盖全部的代码。通过手写代码的方式，我们能够更细粒度的控制追踪的实现。\n* ZipKin默认是需要独立存储的，对于常年运行的系统来说，无论是运维还是机器，都会造成一定的浪费。在我们的架构下，会把追踪与日志进行融合，节省Zipkin带来的额外成本。\n* 打日志时会自动带上TraceId，让调试和定位问题更加方便。\n\n## 利用Logback的MDC机制存储TraceId \n\n前面已经提到，我们想要将TraceId追加到日志系统中。\n\n幸运的是，Logback中提供了[Mapped Diagnostic Context](https://logback.qos.ch/manual/mdc.html)的功能，我们可以将一些变量存储到MDC中，在打日志中，将它打印出来。\n\n要说明的是，MDC是线程独立、线程安全的，而在我们的架构中，无论是HTTP还是RPC请求，都是在各自独立的线程中完成的，与MDC的机制可以很好地契合。\n\n我们来看一下TraceId的存取：\n```java\n\nimport org.slf4j.MDC;\n\npublic class TraceIdContext {\n\n    public static final String TRACE_ID_KEY = \"TRACE_ID\";\n\n    public static void setTraceId(String traceId) {\n        if (traceId != null && !traceId.isEmpty()) {\n            MDC.put(TRACE_ID_KEY, traceId);\n        }\n    }\n\n    public static String getTraceId() {\n        String traceId = MDC.get(TRACE_ID_KEY);\n        if (traceId == null) {\n            return \"\";\n        }\n        return traceId;\n    }\n\n    public static void removeTraceId() {\n        MDC.remove(TRACE_ID_KEY);\n    }\n}\n```\n\n如上所示: 我们直接调用MDC的put, get , remove方法完成了traceId（TraceId）的存取\n\ntraceId可以根据需求随机生成:\n```java\n\nimport java.util.Random;\n\n/**\n * @author coder4\n */\npublic class TraceIdUtils {\n\n    private static final Random random = new Random(System.currentTimeMillis());\n\n    public static String getTraceId() {\n        // 随机正整数的16进制化\n        return Long.toString(Math.abs(random.nextLong()), 16);\n    }\n\n}\n```\n\n如上所属，我们随机生成正整数，并将其格式化为16进制字符串，方便查看。\n\n至于TraceId的生成时机，我们稍后进行讨论。\n\n## 调用TraceId的全新生成\n\n根据前面的描述，应该可以想到，当TraceId为空的情况下，我们需要生成一个新的TraceId。\n\n换句话说，当访问是\"源头\"的情况下，标志着一次追踪的开始，例如：\n* HTTP请求开始之前\n* 消息队列监听器接收新消息时\n\n\n下面来看一下实现。首先，我们可以通过Filter机制，实现HTTP请求中的TraceId分配：\n```java\nimport org.springframework.web.filter.AbstractRequestLoggingFilter;\n\nimport javax.servlet.http.HttpServletRequest;\n\npublic class TraceIdRequestLoggingFilter extends AbstractRequestLoggingFilter {\n    @Override\n    protected void beforeRequest(HttpServletRequest request, String message) {\n        TraceIdContext.setTraceId(TraceIdUtils.getTraceId());\n    }\n\n    @Override\n    protected void afterRequest(HttpServletRequest request, String message) {\n        TraceIdContext.removeTraceId();\n    }\n}\n\n```\n\n如上所示，我们通过Spring MVC的AbstractRequestLoggingFilter接口，在发起请求之前生成一个全新的TraceId，并在请求结束后清理这个TraceId。\n\n当然，上述Filter需要配合一个自动配置才能生效:\n```java\n\nnfiguration\n@ConditionalOnWebApplication\npublic class TraceIdRequestLoggingFilterConfiguration {\n\n    @Bean\n    public TraceIdRequestLoggingFilter createTraceIdMDCFilter() {\n        return new TraceIdRequestLoggingFilter();\n    }\n\n}\n```\n\n代码比较简单，不再详细讨论了。\n\n在消息队列的事件监听器中，也可以采取类似的方法新建TraceId：\n```java\n\npublic void onMessage(Message msg) {\n    TraceIdContext.setTraceId(TraceIdUtils.getTraceId());\n    // do message process\n    TraceIdContext.removeTraceId();\n}\n\n```\n\n当然，如果对每个事件监听器都做上述处理，未免有些麻烦，可以使用抽象基类或者AOP的方式统一，这里不再详细展开。\n\n## TraceId的传递\n\n前面说了TraceId的全新生成，在另外一些情况中，只需要继承环境中已有的TraceId，不需要重新生成，例如：\n* RPC调用，一般情况是在HTTP请求中、或者消息队列中发起，此时系统中已有了一个TraceId\n* 服务内各类之间的相互调用，由于并不是与外界隔离的入口，一般都已经存在了一个TraceId，所以也不需要生成。\n\n前面已经提到，每次完整请求都是在各自独立的线程中完成的，因此\"服务内各类之间\"的相互调用，不需要额外处理，直接从MDC获取TraceId即可。\n\n我们重点看一下RPC中，如何传递TraceId。\n\n我们的技术架构使用了Thrife RPC，可以通过自定义协议的方式，将TraceId自动传递过去：\n```java\n\nimport com.coder4.sbmvt.trace.TraceIdContext;\nimport com.coder4.sbmvt.trace.TraceIdUtils;\nimport org.apache.thrift.TException;\nimport org.apache.thrift.protocol.TBinaryProtocol;\nimport org.apache.thrift.protocol.TField;\nimport org.apache.thrift.protocol.TProtocol;\nimport org.apache.thrift.protocol.TProtocolFactory;\nimport org.apache.thrift.protocol.TProtocolUtil;\nimport org.apache.thrift.protocol.TType;\nimport org.apache.thrift.transport.TTransport;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * @author coder4\n */\npublic class TraceBinaryProtocol extends TBinaryProtocol {\n\n    public static final short TRACE_ID_FIELD = Short.MAX_VALUE;\n\n    private Logger LOG = LoggerFactory.getLogger(getClass());\n\n    public TraceBinaryProtocol(TTransport trans) {\n        super(trans);\n    }\n\n    public TraceBinaryProtocol(TTransport trans, boolean strictRead, boolean strictWrite) {\n        super(trans, strictRead, strictWrite);\n    }\n\n    public TraceBinaryProtocol(TTransport trans, long stringLengthLimit,\n                               long containerLengthLimit, boolean strictRead,\n                               boolean strictWrite) {\n        super(trans, stringLengthLimit, containerLengthLimit, strictRead, strictWrite);\n    }\n\n    @Override\n    public void writeFieldStop() throws TException {\n        // get traceId from context\n        String traceId = TraceIdContext.getTraceId();\n        if (traceId == null || traceId.isEmpty()) {\n            // generate new one if not avaliable\n            traceId = TraceIdUtils.getTraceId();\n            TraceIdContext.setTraceId(traceId);\n        }\n        // parse traceId\n        TField field = new TField(\"\", TType.STRING, TRACE_ID_FIELD);\n        writeFieldBegin(field);\n        writeString(traceId);\n        writeFieldEnd();\n        // super\n        super.writeFieldStop();\n    }\n\n    @Override\n    public TField readFieldBegin() throws TException {\n        // super\n        TField field = super.readFieldBegin();\n        // read traceId\n        while (true) {\n            switch (field.id) {\n                case TRACE_ID_FIELD:\n                    if (field.type == TType.STRING) {\n                        // set traceId to context\n                        String traceId = readString();\n                        TraceIdContext.setTraceId(traceId);\n                        readFieldEnd();\n                    } else {\n                        TProtocolUtil.skip(this, field.type);\n                        LOG.error(\"traceId field type is not string\");\n                    }\n                    break;\n                default:\n                    return field;\n            }\n\n            field = super.readFieldBegin();\n        }\n    }\n\n    public static class Factory extends TBinaryProtocol.Factory implements TProtocolFactory {\n\n        public Factory() {\n            super();\n        }\n\n        public Factory(boolean strictRead, boolean strictWrite) {\n            super(strictRead, strictWrite);\n        }\n\n        public Factory(boolean strictRead, boolean strictWrite, long stringLengthLimit, long containerLengthLimit) {\n            super(strictRead, strictWrite, stringLengthLimit, containerLengthLimit);\n        }\n\n        @Override\n        public TProtocol getProtocol(TTransport trans) {\n            TraceBinaryProtocol protocol =\n                    new TraceBinaryProtocol(trans, stringLengthLimit_, containerLengthLimit_,\n                            strictRead_, strictWrite_);\n\n            return protocol;\n        }\n    }\n}\n\n\n```\n\n如上所示，我们继承了TBinaryProtocol，实现了TraceBinaryProtocol。\n\n* 它在writeFieldStop即写完其他字段后，追加了一个特殊字段TRACE_ID。字段TRACE_ID对应的值，首先会从MDC中获取，若取不到则需要重新生成。\n* 类似地在服务读取阶段，会检查有无TRACE_ID字段，若有将它写入到当前MDC环境中。\n\n## TraceID的展示\n\n经过上面的努力，在我们的架构下，所有请求相关的处理，都会自动带上一个TRACE_ID，我们再来看一下如何将其展示在日志中：\n\n\n我们在logback的Pattern中添加\"[tr=%mdc{TRACE_ID:-0}]\"一项，表示从MDC中获取key为TRACE_ID的数据，若取不到则打印0。\n\n完整的Pattern如下：\n```xml\n<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] [%thread] [%logger] [tr=%mdc{TRACE_ID:-0}] %msg %n</Pattern>\n```\n\n经过上述修改后，你可以重启一下服务，访问REST或者RPC接口。\n\n你会发现，不同的请求中，[tc=xxx]中的TraceId会发生变化。但在同一次请求中调用了多个类，则TraceId会保持、传递下去。\n"
  },
  {
    "path": "legacy/ms-monitor/README.md",
    "content": "# 微服务监控\n\n"
  },
  {
    "path": "legacy/ms-monitor/k8s-prometheus-grafana.md",
    "content": "# Kubernetes + Prometheus + Grafana监控平台\n\n平台监控是微服务架构中的重要一环。\n\n例如，一个很常见的场景，某个微服务突然响应变慢，之前都是100毫米内返回，现在需要2秒才能返回结果，导致大量下游服务超时，究竟出了什么问题呢？\n\n可能的原因有很多，举几个常见的例子：\n* 物理机出现了问题，被别的任务影响，占用了CPU。\n* 微服务的某一个副本出现了Bug，导致死循环，响应变慢\n* 微服务出现Full GC，导致响应变慢\n\n可能的原因还有很多，那么，究竟是哪种原因导致的呢？\n\n我们可以通过日志去查找，但查找起来很费时。而且，对于收到物理机影响等部分请款，是无法在日志中体现出来的。\n\n此时，就是监控平台大显身手的时刻了。监控系统会收集系统中的各项性能指标，按照类型及时间进行聚合，并通过图形化界面的方式呈现出来，让我们对系统的基本运行状况一目了然，便于快速发现当前问题、查找历史问题。\n\n对于监控平台，已经有很多优秀的开源解决方案，例如传统的Zabbix、Nagios，但这些系统比较复杂，一般需要较为专业的运维人员才能上手。\n\n本书选用较为轻量级的Prometheus + Grafana实现监控平台。\n\nPrometheus是一款开源的性能监控、预警系统，他的特点有：\n* 多维数据源、时间聚合\n* 支持高级查询语句\n* 支持单击和多级存储，不依赖其他分布式系统\n* 数据源、服务器均支持多种配置、自动发现方式\n\n上述特点没有提到可视化部分，是的，你已经猜到了，Prometheus只负责收集、存储、查询数据，并不包含数据可视化的部分。\n\n一般可以通过Grafana实现监控数据的可视化，效果还是非常炫酷的，如下图所示：\n\n![Grafana可视化](./grafana-node.png)\n\n上图通过曲线图和仪表盘的方式，展示了k8s集群中，某台物理机的性能状况，CPU、系统负载、内存、网络等状况一目了然。\n\n本节将主要探讨如何将Kuberntes、Prometheus、Grafana整合在一起。\n\n## 前期准备\n\n在整合监控平台前，你首先需要有一个真正的Kubernetes集群，我们假设你已经搭建了一个有两个结点的集群，一台是master，另一台是slave。\n\n如果你不了解如何搭建k8s集群，可以参考[《搭建Kubernets集群》](../devops/k8s-cluster.md)一节。\n\n在本文提供的方案中，Grafana和Prometheus默认都是架设在Kubernetes集群内的，因此，你需要打通本地和集群网络，如果你没有头绪，可以参考[《OpenVPN访问Kubernetes集群内网》](../devops/openvpn-k8s.md)一节。\n\n上述技术准备妥当后，我们来开始搭建监控平台。\n\n## 搭建监控平台\n\n首先，我们要安装helm，这是Kubernetes的包管理系统，类似于Ubuntu中apt的地位。\n\n```shell\nwget https://storage.googleapis.com/kubernetes-helm/helm-v2.9.1-linux-amd64.tar.gz\ntar -xvf ./helm-v2.9.1-linux-amd64.tar.gz\ncd linux-amd64/\n\n```\n\nhelm需要初始化才能工作，但在初始化前，先需要在rbac中进行授权, tiller-rbac.yaml：\n```yaml\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: tiller\n  namespace: kube-system\n---\napiVersion: rbac.authorization.k8s.io/v1beta1\nkind: ClusterRoleBinding\nmetadata:\n  name: tiller\nroleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: ClusterRole\n  name: cluster-admin\nsubjects:\n  - kind: ServiceAccount\n    name: tiller\n    namespace: kube-system\n```\n\n授权比较简单\n```shell\n\nkubectl apply -f ./tiller-rbac.yaml\n\n```\n\n有了权限后，我们进行初始化，并添加一下第三方源\n```shell\nhelm init --service-account tiller\nhelm repo add coreos https://s3-eu-west-1.amazonaws.com/coreos-charts/stable/\n```\n\n上述执行成功后，我们开始创建prometheus：\n```shell\nhelm install coreos/prometheus-operator --name prometheus-operator --namespace monitoring\n```\n\n创建成功后，再创建grafana，并配置其和prometheus关联：\n```shell\nhelm install coreos/kube-prometheus --name kube-prometheus --set global.rbacEnable=true --namespace monitoring\n```\n\n由于涉及到的Pod比较多，下载镜像的时间比较长，全部下载完成后，状态应该如下所示：\n```shell\n\nkubectl get pod -n monitoring\n\nNAME                                                  READY     STATUS    RESTARTS   AGE\nalertmanager-kube-prometheus-0                        2/2       Running   0          10m\nkube-prometheus-exporter-kube-state-66bccfc84-x4ngb   2/2       Running   0          10m\nkube-prometheus-exporter-node-62phq                   1/1       Running   0          10m\nkube-prometheus-exporter-node-lt954                   1/1       Running   0          10m\nkube-prometheus-grafana-f869c754-44f9k                2/2       Running   0          10m\nprometheus-kube-prometheus-0                          3/3       Running   1          10m\nprometheus-operator-858c485-fkcjz                     1/1       Running   0          1h\n\n```\n\n对外暴露的Service如下：\n```shell\nkubectl get service -n monitoring\nNAME                                  TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)             AGE\nalertmanager-operated                 ClusterIP   None             <none>        9093/TCP,6783/TCP   11m\nkube-prometheus                       ClusterIP   10.111.94.74     <none>        9090/TCP            11m\nkube-prometheus-alertmanager          ClusterIP   10.109.44.85     <none>        9093/TCP            11m\nkube-prometheus-exporter-kube-state   ClusterIP   10.105.121.198   <none>        80/TCP              11m\nkube-prometheus-exporter-node         ClusterIP   10.96.155.209    <none>        9100/TCP            11m\nkube-prometheus-grafana               ClusterIP   10.109.181.200   <none>        80/TCP              11m\nprometheus-operated                   ClusterIP   None             <none>        9090/TCP            11m\n\n```\n\n其中，kube-prometheus-grafana的10.109.181.200就是Grafana的Service IP地址。\n\n## 监控系统展示\n\n在打通k8s集群内网后，我们直接打开浏览器访问\"10.109.181.200\"，即可进入Grafana图形化监控系统。\n\n![Grafana可视化](./grafana-pod.png)\n\n![Grafana可视化](./grafana-statefulset.png)\n\n如上所示，默认会从物理机(node)、容器(pod)、容器合集(statefulset)、k8s集群四个层次展示，点击左上角的按钮可以切换展示级别。点击顶部筛选条可以切换具体的机器、容器等实体。\n\n## 拓展与思考\n1. 在本文中，我们实现了对集群中资源、实体的监控，在实际应用中，还想对微服务进行监控，例如REST接口的.99响应、错误码4xx、5xx数量。如何完成这项工作呢？请自行查找资料，并实现这类功能。\n1. Prometheus除了收集监控指标外，还支持报警。如果我们想实现物理机内存占用大于90%，自动发邮件(或短信)报警，如何实现呢？请自行查找资料，实现这类功能。\n"
  },
  {
    "path": "legacy/ms-monitor/sb-prometheus.md",
    "content": "# 整合Prometheus\n\n"
  },
  {
    "path": "legacy/ms-monitor/sb-sentry.md",
    "content": "# Spring Boot整合Sentry\n\n在上一小节中，我们探讨了如何运维Senty系统。\n\n搭建好的Sentry系统，需要接入错误事件的数据源，才能发挥功效。\n\n在本节中，我们探讨如何将Spring Boot的微服务项目与Sentry整合起来。\n\n## Sentry中新建项目\n\nSentry中可以新建若干个项目，对应于若干微服务。\n\n而同一微服务的若干副本应该向同一个Sentry项目发送错误信息的数据，这些副本可以通过来源IP区分。\n\n我们来新建一个Sentry项目，如下图所示：\n\n![Sentry中新建项目](./sentry-create-proj.png)\n\n首先选择一个项目类型，这里选择Java\n\n接着，选择项目名称，我们这里和微服务的名字保持一致lmsia-abc。\n\n新建好项目后，首页会出现项目的列表，及24小时内，该项目的错误事件数量，如下图所示：\n\n![Sentry首页展示项目](./sentry-proj2.png)\n\n## 在Spring Boot中配置日志输出\n\nSentry提供了丰富多样的接入方式。\n\n在本书中我们采用了Spring Boot默认的logback作为日志系统，Sentry也支持这种方式。\n\n首先添加依赖：\n```grovvy\n    compile 'io.sentry:sentry-logback:1.7.5'\n```\n\n然后将在logback的配置修改如下：\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n\n    <!-- For console -->\n    <appender name=\"ConsoleAppender\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <charset>UTF-8</charset>\n            <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%level] [%thread] [%logger] [tr=%mdc{TRACE_ID:-0}] %msg %n</Pattern>\n        </encoder>\n    </appender>\n\n    <!-- For file with daily rollover -->\n    <appender name=\"ServerFileAppender\" class=\"ch.qos.logback.core.rolling.RollingFileAppender\">\n        <encoder>\n            <charset>UTF-8</charset>\n            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%level] [%thread] [%logger] [tr=%X{TRACE_ID:-0}] %msg %n</pattern>\n        </encoder>\n\n        <file>/app/logs/lmsia-abc.log</file>\n\n        <rollingPolicy class=\"ch.qos.logback.core.rolling.TimeBasedRollingPolicy\">\n            <!-- daily rollover with gz -->\n            <fileNamePattern>/app/logs/lmsia-abc.%d{yyyy_MM_dd}.log.gz</fileNamePattern>\n            <!-- keep 30 days' max -->\n            <maxHistory>30</maxHistory>\n        </rollingPolicy>\n    </appender>\n\n    <!-- For sentry -->\n    <appender name=\"SentryAppender\" class=\"io.sentry.logback.SentryAppender\">\n        <filter class=\"ch.qos.logback.classic.filter.ThresholdFilter\">\n            <level>ERROR</level>\n        </filter>\n    </appender>\n\n    <!-- console only if local active -->\n    <springProfile name=\"local\">\n        <root level=\"INFO\">\n            <appender-ref ref=\"ConsoleAppender\"/>\n        </root>\n    </springProfile>\n    <!-- file,sentry only if test or online active -->\n    <springProfile name=\"test,online\">\n        <root level=\"INFO\">\n            <appender-ref ref=\"ServerFileAppender\"/>\n            <appender-ref ref=\"SentryAppender\"/>\n        </root>\n    </springProfile>\n\n</configuration>\n```\n\n与之前的配置文件相比，主要改动为：\n* 新增SentryAppender。它会自动沿用之前最近的一个Pattern，及这里的ServerFileAppender。另外，只有ERROR级别的LOG，才会加到这个Appender中。\n* 当test或者online的profile激活时，自动上报。你可能希望test和online上报到不同的项目中，别着急，我们后面解决这种情况。\n\n经过上述配置后，每当发生ERROR级别的LOG，就会追加到SentryAppender中。\n\n## Spring Boot中配置DSN\n\n在刚才的配置中，我们并没有制定Sentry服务的IP和端口，如何让Spring Boot知道事件要发送到哪里呢？\n\n此外前面提到，Sentry中支持配置多个项目，如何告诉Spring Boot要发送到Sentry的哪个项目中呢？\n\n这就需要DSN出场了，DSN是Sentry创建项目时生成的一个“KEY”，用于标识和区分不同的项目。\n\n可以在项目的Setting中找到它，如下图所示：\n\n![Sentry项目的DSN](./sentry-dsn.png)\n\n然后，我们在Spring Boot项目的resources目录下，新建sentry.properties文件\n```\ndsn = http://9445296bd1a5441c8988af84044890a3@sentry-host:sentry-port/2\n```\n\n如上配置后，就可以识别出sentry服务的位置已经对应的项目名了。\n\n要说明的是，sentry-host和sentry-port要根据你的需要自行修改，可以是IP也可以是可DNS解析的域名。\n\n如果你想要区分test和online环境，可以如下操作：\n* 建立不同的Sentry服务，或者同一个Senty服务下建立不同的项目\n* 根据不同的profile分别创建sentry.properties文件，如sentry.properties.test, sentry.properties.online，里面配置不同的dsn key\n* 打Docker镜像时加载不同的文件，并重命名为sentry.properties\n\n## 实验异常发生的效果\n\n为了实验效果，我们在微服务代码中，主动抛出一个异常，然后看一下Sentry的lmsia-abc项目：\n\n![Sentry项目的异常预警](./sentry-err.png)\n\n可以看到，我们的异常被Sentry捕获，并显示了出来。如果同样的ERROR级别LOG发生了多次，还会自动聚合。\n\n至此，我们完成了Spring Boot与Sentry的整合工作。\n"
  },
  {
    "path": "legacy/ms-monitor/sentry-devops.md",
    "content": "# Sentry 错误预警系统的运维\n\n在上一章中，我们介绍了EBLK的日志分析平台。\n\n在日志分析平台上，我们可以很方便的查找系统的日志。然而，EBLK并总是能满足需求：\n* 日志绝大多数是INFO等级的，即信息日志。如果系统运行出现问题，我们想从中查找，实际希望的是找到ERROR类型的或者异常信息。\n* 我们经常需要排查一些历史故障，即需要增加时间维度的信息\n* 我们希望经常出现的类似错误，能够聚合在一起，方便我们排查\n\nSentry是一个实时的事件日志和聚合平台，我们可以通过配置，把所有ERROR类型的日志发送给Sentry保存下来，并通过其聚合结果，迅速的定位线上问题。\n\n在本节中，我们将探讨Sentry预警系统的运维工作。\n\n由于Sentry服务本身比较复杂，涉及多个步骤初始化步骤及多个Volume，因此我们不再采用Kubernetes集群，而是通过Docker直接部署在某台物理机上。\n\n## Sentry存储依赖的部署\n\nSentry服务需要使用两个存储：Redis、Postgres，我们首先启动这两个依赖服务。\n\n首先创建redis, sentry-redis.sh:\n```shell\n#!/bin/bash\n\nNAME=\"sentry-redis\"\nVOLUME=\"/home/coder4/docker_data/sentry-redis\"\n\n# make sure volume valid \nmkdir -p $VOLUME && sudo chmod -R 777 $VOLUME\n\n# kill old and run new \ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    --volume \"$VOLUME\":/data \\\n    --detach \\\n    --restart always \\\n    redis:4 \n```\n\n如上所述:\n* 创建了基于redis 4的容器\n* 设置volume到/data，这样，重启redis并不会导致数据丢失\n\n接着，我们创建Postegres数据库, sentry_postgres.sh:\n```shell\n#!/bin/bash\n\nNAME=\"sentry-postgres\"\nVOLUME=\"/home/coder4/docker_data/sentry-postgres\"\n\nPOSTGRES_DB_USER=\"sentry\"\nPOSTGRES_DB_PASS=\"sentry_pass\"\n\n# make sure volume valid \nsudo mkdir -p $VOLUME && sudo chmod -R 777 $VOLUME\n\n# kill old and run new \ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    --volume \"$VOLUME\":/var/lib/postgresql/data \\\n    --env POSTGRES_USER=$POSTGRES_DB_USER \\\n    --env POSTGRES_PASSWORD=$POSTGRES_DB_PASS \\\n    --detach \\\n    --restart always \\\n    postgres:10 \n\n```\n\n## Sentry 服务的部署 \n\n在正式部署Sentry之前，首先要生成key：\n\n```shell\ndocker run --rm sentry config generate-secret-key\n```\n\n输出结果是\n```\nq5%_k4t#w#43rlnd(mr1ms%p8(mjofh9z&4al8d1q&a3f#19_d\n```\n\n记住这个key，我们马上会用到。\n\n下面，我们对Sentry进行初始化:\n```shell\n#!/bin/bash\n\nNAME=\"sentry-main\"\nREDIS_LINK=\"sentry-redis:redis\"\nPOSTGRES_LINK=\"sentry-postgres:postgres\"\n\nSENTRY_SECRET=\"q5%_k4t#w#43rlnd(mr1ms%p8(mjofh9z&4al8d1q&a3f#19_d\"\n\n# kill old and run new \ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    --env SENTRY_SECRET_KEY=$SENTRY_SECRET \\\n    -it \\\n    --restart always \\\n    --link $REDIS_LINK \\\n    --link $POSTGRES_LINK \\\n    sentry:9 upgrade\n\n```\n\n在执行db操作一段时间后，会有一个交互，如下：\n```\nEmail: lihy@coder4.com\nPassword: \nRepeat for confirmation: \nShould this user be a superuser? [y/N]: y\nUser created: lihy@coder4.com\n...\n```\n创建好的用户，就是之后默认的管理员用户\n\n初始化完毕后，我们正式创建Sentry:\n```shell\n#!/bin/bash\n\nNAME=\"sentry-main\"\nREDIS_LINK=\"sentry-redis:redis\"\nPOSTGRES_LINK=\"sentry-postgres:postgres\"\n\nSENTRY_SECRET=\"q5%_k4t#w#43rlnd(mr1ms%p8(mjofh9z&4al8d1q&a3f#19_d\"\n\n# kill old and run new \ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    -p 8080:9000 \\\n    --env SENTRY_SECRET_KEY=$SENTRY_SECRET \\\n    --detach \\\n    --restart always \\\n    --link $REDIS_LINK \\\n    --link $POSTGRES_LINK \\\n    sentry:9\n\n```\n\n与上面的初始化相比，脚本基本是类似的，差别在于：\n* 使用的是后台运行detach而非-it交互模式\n* 没有执行upgrade命令\n* 开放了8080端口\n\n执行成功后，我们访问http://localhost:8080，即可成功进入登录界面，如下图所示：\n\n![Sentry登录界面](./sentry-login.png)\n\n如果你现在登录的话，会提示\"Background workers havn't checked in recently...\"，即后台收集进程&定时任务没有启动。\n\n我们首先来启动收集进程:\n\n```shell\n#!/bin/bash\n\nNAME=\"sentry-worker\"\nREDIS_LINK=\"sentry-redis:redis\"\nPOSTGRES_LINK=\"sentry-postgres:postgres\"\n\nSENTRY_SECRET=\"q5%_k4t#w#43rlnd(mr1ms%p8(mjofh9z&4al8d1q&a3f#19_d\"\n\n# kill old and run new \ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    --env SENTRY_SECRET_KEY=$SENTRY_SECRET \\\n    --detach \\\n    --restart always \\\n    --link $REDIS_LINK \\\n    --link $POSTGRES_LINK \\\n    sentry:9 run worker\n\n```\n\n然后启动定时任务：\n```shell\n#!/bin/bash\n\nNAME=\"sentry-cron\"\nREDIS_LINK=\"sentry-redis:redis\"\nPOSTGRES_LINK=\"sentry-postgres:postgres\"\n\nSENTRY_SECRET=\"q5%_k4t#w#43rlnd(mr1ms%p8(mjofh9z&4al8d1q&a3f#19_d\"\n\n# kill old and run new \ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    --env SENTRY_SECRET_KEY=$SENTRY_SECRET \\\n    --detach \\\n    --restart always \\\n    --link $REDIS_LINK \\\n    --link $POSTGRES_LINK \\\n    sentry:9 run cron \n\n```\n\n启动后稍等一会，我们用初始化时设置的用户名、密码登录系统，进入初始化界面：\n\n![Sentry初始配置界面](./sentry-config.png)\n\n配置如下：\n* Root URL写入一个可以DNS解析的域名，例如sentry.coder4.com\n* Admin Email写入任意一个邮件地址，例如sentry@coder4.com\n\n配置好后，进入如下的登台添加新项目界面，至此，Sentry的部署工作配置完成。\n\n![Sentry完成界面](./sentry-ready.png)\n\n## 思考与拓展\n* 在本节的部署中，我们使用了默认的账户配置模式。前面介绍过，在团队内部，应该使用统一的帐号系统以提升效率。如何让Sentry系统接入LDAP帐号服务呢，请自行查找资料，实现这个功能。\n"
  },
  {
    "path": "legacy/ms-monitor/sentry.txt",
    "content": "https://laravel-china.org/articles/4285/build-your-own-sentry-service\n"
  },
  {
    "path": "legacy/ms-msgq/README.md",
    "content": "# 微服务的消息队列\n\n"
  },
  {
    "path": "legacy/ms-msgq/dev-kafka.md",
    "content": "# Kafka 流处理开发简介\n\n在上一节，我们介绍了分布式流处理平台Kafka的运维工作，在这一节，我们将讨论Kafka的应用开发。\n\n你可能已经注意到，这一节的标题并不是\"在微服务中的集成\"，而是\"开发简介\"。\n\n使得，如在前文所述，Kafka的吞吐性能出色，但延迟性能一般，因此多用于离线处理的场景，典型应用有：\n* 异源数据[^1]的同步、转换、备份。即\"数据搬运工\"\n* 处理从多处收集的日志\n* Event Sourcing模式的事件回放\n\n对于数据搬运工类的需求，建议优先考虑[Kafka Connect](http://kafka.apache.org/documentation/#connect_overview)。这是Kafka官方提供的一组工具集，可以通过简单的配置，就完成数据的同步和一些转换的工作。\n\n对于Event Sourcing的模式开发，和日志处理收集的开发模式基本一致。\n\n在此，我们用较短的篇幅，对Kafka的开发做一个简要的介绍。\n\n在研究代码之前，有一些必须明确的基本概念：\n* Topics: 可以理解为队列，同种消息应放入相同的Topic内。\n* Partition: Topic下可划分为多个分区， 便于并行处理。\n* Replicas: Topic的Partition可以划分为多个副本，从而实现高可用保证。\n* Producer: 消息的生产者。\n* Consumer: 消息的消费者。\n* Consumer Group: 每个消费者可以设定一个Consumer Group。Kafka保证: 同一个消息，只投递给相同Consumer Group中的一台Consumer。换句话说，注册在同一个Consumer Group下的Consumer，可以并行的处理消息。\n\n下面，我们看一下生产者\n```\n//import util.properties packages\nimport java.util.Properties;\n\n//import simple producer packages\nimport org.apache.kafka.clients.producer.Producer;\n\n//import KafkaProducer packages\nimport org.apache.kafka.clients.producer.KafkaProducer;\n\n//import ProducerRecord packages\nimport org.apache.kafka.clients.producer.ProducerRecord;\n\n//Create java class named “SimpleProducer\"\npublic class SimpleProducer {\n   \n   public static void main(String[] args) throws Exception{\n      \n      // Check arguments length value\n      if(args.length == 0){\n         System.out.println(\"Enter topic name\");\n         return;\n      }\n      \n      //Assign topicName to string variable\n      String topicName = args[0].toString();\n      \n      // create instance for properties to access producer configs   \n      Properties props = new Properties();\n      \n      //Assign localhost id\n      props.put(\"bootstrap.servers\", \"localhost:9092\");\n      \n      //Set acknowledgements for producer requests.      \n      props.put(\"acks\", \"all\");\n      \n      //If the request fails, the producer can automatically retry,\n      props.put(\"retries\", 0);\n      \n      //Specify buffer size in config\n      props.put(\"batch.size\", 16384);\n      \n      //Reduce the no of requests less than 0   \n      props.put(\"linger.ms\", 1);\n      \n      //The buffer.memory controls the total amount of memory available to the producer for buffering.   \n      props.put(\"buffer.memory\", 33554432);\n      \n      props.put(\"key.serializer\", \n         \"org.apache.kafka.common.serializa-tion.StringSerializer\");\n         \n      props.put(\"value.serializer\", \n         \"org.apache.kafka.common.serializa-tion.StringSerializer\");\n      \n      Producer<String, String> producer = new KafkaProducer\n         <String, String>(props);\n            \n      for(int i = 0; i < 10; i++)\n         producer.send(new ProducerRecord<String, String>(topicName, \n            Integer.toString(i), Integer.toString(i)));\n               System.out.println(\"Message sent successfully\");\n               producer.close();\n   }\n}\n\n```\n\n简单解释下代码：\n1. 连接localhost:9092\n1. 收到消息后自动ack\n1. 设置缓存大小等配置\n1. 消息的key和value都是string类型，配置对应的序列化类\n1. 发送10个消息\n\n下面来看一下Consumer代码\n```java\nimport java.util.Properties;\nimport java.util.Arrays;\nimport org.apache.kafka.clients.consumer.KafkaConsumer;\nimport org.apache.kafka.clients.consumer.ConsumerRecords;\nimport org.apache.kafka.clients.consumer.ConsumerRecord;\n\npublic class ConsumerGroup {\n   public static void main(String[] args) throws Exception {\n      if(args.length < 2){\n         System.out.println(\"Usage: consumer <topic> <groupname>\");\n         return;\n      }\n      \n      String topic = args[0].toString();\n      String group = args[1].toString();\n      Properties props = new Properties();\n      props.put(\"bootstrap.servers\", \"localhost:9092\");\n      props.put(\"group.id\", group);\n      props.put(\"enable.auto.commit\", \"true\");\n      props.put(\"auto.commit.interval.ms\", \"1000\");\n      props.put(\"session.timeout.ms\", \"30000\");\n      props.put(\"key.deserializer\",          \n         \"org.apache.kafka.common.serializa-tion.StringDeserializer\");\n      props.put(\"value.deserializer\", \n         \"org.apache.kafka.common.serializa-tion.StringDeserializer\");\n      KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);\n      \n      consumer.subscribe(Arrays.asList(topic));\n      System.out.println(\"Subscribed to topic \" + topic);\n      int i = 0;\n         \n      while (true) {\n         ConsumerRecords<String, String> records = con-sumer.poll(100);\n            for (ConsumerRecord<String, String> record : records)\n               System.out.printf(\"offset = %d, key = %s, value = %s\\n\", \n               record.offset(), record.key(), record.value());\n      }     \n   }  \n}\n\n```\n\n基本的配置与Producer类似，这里i不再重复了，能够并行处理的关键是\"group.id\"这个参数。\n如果同时启动2个Consumer进程，会发现消息是在两个Consumer进程中，交替输出的。\n\n上述代码参考自[Tutorial Spoint的Kafka教程](https://www.tutorialspoint.com/apache_kafka/)，这是一部非常好的Kafka教程，如果你没有时间完整阅读官方文档，强烈推荐你读一下这部教程。\n\n[^1]: 异源指的是不同数据源，例如MySQL和Kafka之间、HDFS和Kafka之间。\n"
  },
  {
    "path": "legacy/ms-msgq/kafka-devops.md",
    "content": "# Kafka流处理平台的运维\n\nKafka是高性能的分布式流处理平台，它的特点有：\n* 类似于传统的消息队列，为海量流式数据提供了消息发布/订阅模型。\n* 支持容错的流式数据存储。\n* 流式数据的实时处理。\n\nKafka是一款吞吐性能非常优秀的分布式流处理系。虽然吞吐性能优秀，但Kafka的处理延迟较高，一般多用于日志等离线处理，不会用于实时的消息队列系统。\n\n本节将讨论Kafka集群的部署。\n\n与我们之前讨论的MySQL、Memcached等组件稍有不同:\n* Kafka对I/O资源消耗较大，使用Volume挂载的方式，存在一定性能损耗。\n* 并且Kafka本身内置了高可用、集群的功能。\n* Kafka依赖Zookeeper，后者对资源波动较为敏感，一般需要独立部署。\n\n综上所述，对于Kafka和其依赖的ZooKeeper，我们将在服务器上独立部署，而不会将其部署在Kubernetes集群中。\n\n## 准备Java环境\n\n我们假设你手里的是仅安装了操作系统的\"裸机\"服务器，在这里，我们以以Ubuntu 18.04为例进行讲解。\n\n首先准备Java的apt源\n\n```shell\nsudo add-apt-repository -y ppa:webupd8team/java\nsudo apt update\n```\n\n然后，自动同意许可、自动安装\n```shelll\necho debconf shared/accepted-oracle-license-v1-1 select true | sudo debconf-set-selections\necho debconf shared/accepted-oracle-license-v1-1 seen true | sudo debconf-set-selections\nsudo apt install -y oracle-java8-set-default\n```\n\n安装好后，我们验证一下\n```shell\njava -version\njava version \"1.8.0_171\"\nJava(TM) SE Runtime Environment (build 1.8.0_171-b11)\nJava HotSpot(TM) 64-Bit Server VM (build 25.171-b11, mixed mode)\n```\n\n部署Kafka集群至少需要6台机器，3台给Zookeeper，另外3台给Kafka的Broker。\n\n为了说明方便，我们假设6台机器的主机名分别为zk1~zk3，kafka1~kafka3\n\n请在6台机器上都进行上述Java 8的安装。\n\n## 准备主机环境\n\nzk和kafka集群的部署，都依赖2个先决条件：\n* 主机之间必须支持内网访问\n* 内网可以通过hostname直接访问\n\n由于是在同一个机房内部署，所以我们假设上述条件1是满足的。\n\n对于条件2，有多种实现方案，我们这里采用最传统的hostname修改方式。\n\n对于上述6台主机，内网IP分别为:\n* z1~zk3:192.168.0.11 ~ 192.168.0.13\n* kafka1~kafka3:192.168.0.21 ~ 192.168.0.23\n\n则我们修改6台主机的hosts文件如下:\n```shell\nsudo vim /etc/hosts\n\n127.0.0.1    localhost\n#127.0.1.1    zk2\n\n# The following lines are desirable for IPv6 capable hosts\n::1          localhost ip6-localhost ip6-loopback\nff02::1      ip6-allnodes\nff02::2      ip6-allrouters\n\n192.168.0.11 zk1\n192.168.0.12 zk2\n192.168.0.13 zk3\n\n192.168.0.21 kafka1\n192.168.0.22 kafka2\n192.168.0.23 kafka3\n```\n\n修改后，任意一台机器应该都可以通过hostname来ping通其他主机，例如:\n```shell\nzk1$ ping kafka2\n\nPING baidu.com (192.168.0.22) 56(84) bytes of data.\n64 bytes from 192.168.0.22: icmp_seq=1 ttl=47 time=10.0 ms\n...\n\n```\n\n注意，在上面的配置中，我们还去掉127.0.1.1的映射，最终效果是ping也会返回内网ip，而不是127.0.1.1:\n```shell\nping zk2\nPING zk2 (192.168.0.12) 56(84) bytes of data.\n64 bytes from zk2 (192.168.0.12): icmp_seq=1 ttl=64 time=0.046 ms\n\n```\n\n\n## 部署Zookeeper\n\n接下来，我们将在zk1~zk3上部署zookeeper，请确认这三台机器已经安装了Java 8。\n\n首先为zookeeper创建本地用户，在zk1~zk3上分别执行:\n```shell\nuseradd -m zookeeper\n\n```\n\n下载并解压到本地，同样在zk1~zk3上分别执行：\n```shell\nsu zookeeper\ncd $HOME\n\nwget https://archive.apache.org/dist/zookeeper/zookeeper-3.4.12/zookeeper-3.4.12.tar.gz\ntar -xzvf ./zookeeper-3.4.12.tar.gz\nln -s zookeeper-3.4.12 zookeeper\n\nmkdir /home/zookeeper/zookeeper_data\n```\n\n注意最后创建了一个文件夹，用于储存zk的数据文件\n\n为zk1和zk3添加不同的id\n```shell\nzookeeper@zk1:~$ echo \"1\" > /home/zookeeper/zookeeper_data/myid\nzookeeper@zk2:~$ echo \"2\" > /home/zookeeper/zookeeper_data/myid\nzookeeper@zk3:~$ echo \"3\" > /home/zookeeper/zookeeper_data/myid\n```\n\n在zk1~zk3上分别添加配置\n```shell\nvim /home/zookeeper/zookeeper/conf/zoo.cfg\n\ntickTime=2000\ndataDir=/home/zookeeper/zookeeper_data\nclientPort=2181\ninitLimit=5\nsyncLimit=2\nserver.1=zk1:2888:3888\nserver.2=zk2:2888:3888\nserver.3=zk3:2888:3888\n```\n\n在zk1~zk3上启动zookeeper\n```shell\ncd /home/zookeeper/zookeeper/bin\n\n./zkServer.sh start\n```\n\n启动后，可以在zookeeper.out中查看错误输出日志。\n\n如果一切正常，我们用客户端尝试连接。\n```shell\n./zookeeper/bin/zkCli.sh -server zk1:2181,zk2:2181,zk3:2181\n\n....\n[zk: zk1:2181,zk2:2181,zk3:2181(CONNECTED) 0]\n...\n```\n\n如上如果能显示\"CONNECTED\"，就是连接成功了。\n\n我们尝试创建结点，也能成功：\n```shell\n\n[zk: zk1:2181,zk2:2181,zk3:2181(CONNECTED) 5] create /hello world\nCreated /hello\n\n[zk: zk1:2181,zk2:2181,zk3:2181(CONNECTED) 6] get /hello         \nworld\ncZxid = 0x100000002\nctime = Tue Jun 12 11:36:22 UTC 2018\nmZxid = 0x100000002\nmtime = Tue Jun 12 11:36:22 UTC 2018\npZxid = 0x100000002\ncversion = 0\ndataVersion = 0\naclVersion = 0\nephemeralOwner = 0x0\ndataLength = 5\nnumChildren = 0\n```\n\n至此，我们已经完成了zookeeper集群的配置。\n\n## Kafka集群配置\n\n首先为kafka创建本地用户，在kafka1~kafka3上分别执行:\n```shell\nuseradd -m kafka \n\n```\n\n下载kafka并解压缩\n```shell\nsu kafka\ncd $HOME\n\nwget http://www-eu.apache.org/dist/kafka/1.1.0/kafka_2.11-1.1.0.tgz\ntar -xzvf kafka_2.11-1.1.0.tgz\n\nln -s kafka_2.11-1.1.0 kafka \n```\n\n创建数据目录\n```shell\nmkdir /home/kafka/kafka_logs\n```\n\n配置文件(kafka1)\n```shell\nvim kafka/config/server.properties\n\nbroker.id=1\nzookeeper.connect=zk1:2181,zk2:2181,zk3:2181\nlisteners=PLAINTEXT://kafka1:9092\nlog.dirs=/home/kafka/kafka_logs\n```\n\n分别在kafka1~kafka3上启动\n```shell\ncd $HOME\n\nkafka/bin/kafka-server-start.sh -daemon ./kafka/config/server.properties \n```\n\n创建队列(topic)\n```shell\nkafka/bin/kafka-topics.sh --create --zookeeper zk1:2181,zk2:2181,zk3:2181 --replication-factor 2 --partitions 3 --topic topic1\nCreated topic \"topic1\".\n```\n\n查看队列(topic)\n```shell\nkafka/bin/kafka-topics.sh --describe --zookeeper zk1:2181,zk2:2181,zk3:2181 --topic topic1\n\nTopic:topic1\tPartitionCount:3\tReplicationFactor:2\tConfigs:\n\tTopic: topic1\tPartition: 0\tLeader: 2\tReplicas: 2,1\tIsr: 2,1\n\tTopic: topic1\tPartition: 1\tLeader: 1\tReplicas: 1,2\tIsr: 1,2\n\tTopic: topic1\tPartition: 2\tLeader: 2\tReplicas: 2,1\tIsr: 2,1\n\n```\n\n列出所有队列(topic)\n```shell\nkafka/bin/kafka-topics.sh --list --zookeeper zk1:2181,zk2:2181,zk3:2181\n\ntopic1\n```\n\n生产消息\n```shell\nkafka/bin/kafka-console-producer.sh --broker-list kafka1:9092,kafka2:9092,kafka3:9092 --topic topic1\n>a\n>b\n\n```\n\n消费消息\n```shell\nkafka/bin/kafka-console-consumer.sh --zookeeper zk1:2181,zk2:2181,zk3:2181 --topic topic1 --from-beginning\n\na\nb\n```\n\n删除队列\n```shell\nkafka/bin/kafka-topics.sh --delete --zookeeper zk1:2181,zk2:2181,zk3:2181 --topic topic1\n\nTopic topic1 is marked for deletion.\n\n```\n\n至此，我们完成了Kafka的集群配置，更多内容可以参考[Kafka 官方文档](https://kafka.apache.org/documentation/)。\n"
  },
  {
    "path": "legacy/ms-msgq/rabbitmq-devops.md",
    "content": "# RabbitMQ 消息队列\n\nRabbitMQ支持单机部署，也提供了\"高可用\"的集群部署方式，以提升性能和(或)可用性。\n\nRabbitMQ支持两种形式的集群模式：\n* 普通模式: 默认的模式。消息队列的数据结构存在于每个节点上，但实际消息只存储于某一个节点上。这种模式的优点是性能较高。缺点是，一旦存储数据的节点挂掉，消息就暂时不可用了，需要节点启动后才能恢复。\n* 镜像模式: 在普通模式的基础上可配置需要镜像的队列。配置队列后，消息数据会自动同步到集群中的每个节点上。该模式的优点是可用性高，缺点是性能相对较低。\n\n在本节，我们将首先配置普通模式的集群，然后配置镜像队列。\n\n## Kubernetes下配置RabbitMQ集群\n\n与Memcached类似，RabbitMQ集群中的节点，是相互独立的，而不是可替代的，因此我们也使用StatefulSet。\n\n然而，RabbitMQ比Memcached更为复杂，他需要磁盘存储，即在StatefulSet上的Volume。这种情况下，是无法直接创建Volume挂载点的，而是需要先手动创建Persistent Volume(简称PV)，然后再通过Persistent Volume Claim(简称PVC)去关联创建可用的PV。上述过程可以参考Kubernetes的[Persistent Volume文档](https://kubernetes.io/docs/concepts/storage/persistent-volumes/)。\n\n我们首先创建3个PV, pvs.yaml:\n```yaml\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: pv001 \nspec:\n  storageClassName: standard\n  accessModes:\n    - ReadWriteOnce\n  capacity:\n    storage: 20Gi\n  hostPath:\n    path: /data/pv001/\n---\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: pv002 \nspec:\n  storageClassName: standard\n  accessModes:\n    - ReadWriteOnce\n  capacity:\n    storage: 20Gi\n  hostPath:\n    path: /data/pv002/\n---\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: pv003\nspec:\n  storageClassName: standard\n  accessModes:\n    - ReadWriteOnce\n  capacity:\n    storage: 20Gi\n  hostPath:\n    path: /data/pv003/\n```\n\n在这里，我们使用的是minikube做演示，因而直接创建基于路径的Volume，在实际生产中，你可能需要创建支持自动迁移的Volume，具体可以参考[Storage Classes](https://kubernetes.io/docs/concepts/storage/storage-classes/)。\n\n我们应用一下:\n```yaml\nkubectl apply -f pvs.yaml\n\npersistentvolume \"pv001\" created \npersistentvolume \"pv002\" created \npersistentvolume \"pv003\" created \n```\n\n在创建RabbitMQ集群之前，我们先要针对rabnbitmq这个metadata修改rbac。rbac是Kubernetes的安全性访问限制，具体原因可以参考这个[Issue](https://github.com/rabbitmq/rabbitmq-peer-discovery-k8s/issues/15)\n\nrabbitmq-rbac.yaml:\n```yaml\n---\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: rabbitmq\n---\nkind: Role\napiVersion: rbac.authorization.k8s.io/v1beta1\nmetadata:\n  name: rabbitmq\nrules:\n- apiGroups: [\"\"]\n  resources: [\"endpoints\"]\n  verbs: [\"get\"]\n---\nkind: RoleBinding\napiVersion: rbac.authorization.k8s.io/v1beta1\nmetadata:\n  name: rabbitmq\nroleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: Role\n  name: rabbitmq\nsubjects:\n- kind: ServiceAccount\n  name: rabbitmq\n\n```\n\n上述修改了rabbitmq的ServiceSccount、Role、RoleBinding默认安全设置，简要来说是允许其访问get和endpoints这两个Kubernetes提供的API。\n\n我们应用上述修改，修改成功:\n```yaml\nkubectl apply -f ./rabbitmq-rbac.yaml\n\nserviceaccount \"rabbitmq\" configured\nrole.rbac.authorization.k8s.io \"rabbitmq\" configured\nrolebinding.rbac.authorization.k8s.io \"rabbitmq\" configured\n\n```\n\n在创建了pv，修改了rbac后，可以创建rabbitmq集群了，我们来看一下描述文件,rabbitmq-service.yaml:\n```yaml\nkind: Service\napiVersion: v1\nmetadata:\n  name: rabbitmq\n  labels:\n    app: rabbitmq\nspec:\n  type: NodePort\n  ports:\n  - name: http\n    protocol: TCP\n    port: 15672\n    targetPort: 15672\n    nodePort: 31672\n  selector:\n    app: rabbitmq\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n  name: rabbitmq\nspec:\n  selector:\n    matchLabels:\n      app: rabbitmq\n  serviceName: rabbitmq\n  replicas: 3\n  template:\n    metadata:\n      labels:\n        app: rabbitmq\n    spec:\n      serviceAccountName: rabbitmq\n      terminationGracePeriodSeconds: 10\n      containers:        \n      - name: rabbitmq-autocluster\n        image: pivotalrabbitmq/rabbitmq-autocluster\n        ports:\n          - name: http\n            protocol: TCP\n            containerPort: 15672\n          - name: amqp\n            protocol: TCP\n            containerPort: 5672\n        livenessProbe:\n          exec:\n            command: [\"rabbitmqctl\", \"status\"]\n          initialDelaySeconds: 30\n          timeoutSeconds: 5\n        readinessProbe:\n          exec:\n            command: [\"rabbitmqctl\", \"status\"]\n          initialDelaySeconds: 10\n          timeoutSeconds: 5\n        volumeMounts:\n        - mountPath: /var/lib/rabbitmq/mnesia\n          name: rabbitmq-pvc\n        env:\n          - name: MY_POD_IP\n            valueFrom:\n              fieldRef:\n                fieldPath: status.podIP\n          - name: RABBITMQ_USE_LONGNAME\n            value: \"true\"\n          - name: RABBITMQ_NODENAME\n            value: \"rabbit@$(MY_POD_IP)\"\n          - name: AUTOCLUSTER_TYPE\n            value: \"k8s\"\n          - name: AUTOCLUSTER_DELAY\n            value: \"10\"\n          - name: K8S_ADDRESS_TYPE\n            value: \"ip\" \n          - name: AUTOCLUSTER_CLEANUP\n            value: \"true\"\n          - name: CLEANUP_WARN_ONLY\n            value: \"false\"\n  volumeClaimTemplates:\n  - metadata:\n      name: rabbitmq-pvc\n    spec:\n      storageClassName: standard\n      accessModes:\n        - ReadWriteOnce\n      resources:\n        requests:\n          storage: 20Gi\n\n```\n\n解释一下上面的描述文件：\n* 创建了Headless Service用于暴露管理界面的Web端口31672。这里主要是为了演示，在实际应用中，这个可能是没必要的。\n* 创建了rabbitmq的StatefulSet，含有3个节点，其中每个节点通过livenessProbe和readinessProbe检查可用性。 \n* 使用volumeClaimTemplates自动生成volumeClaim，这里的template即模板，会自动给StatefulSet中的每一个节点创建一个Volume Claim，命名为rabbitmq-pvc-0/1/2 \n\n下面我们来应用下上面的描述:\n```\nkubectl apply -f ./rabbitmq-service.yaml\n\nservice \"rabbitmq\" created\nstatefulset.apps \"rabbitmq\" created\n\n```\n\n稍过一会后，我们来看一下Web服务器的管理界面，http://192.168.99.100，用户名密码都是guest:\n\n![查看RabbitMQ集群](./rabbitmq-cluster-status.png \"查看RabbitMQ集群\")\n\n登录成功后，如上图所示。不难发现，已经成功启动了3个节点，并组成了集群，至此，我们的RabbitMQ基本集群配置成功。\n\n## 配置镜像集群\n\n下面，我们来配置镜像策略，在Web管理工具上点击\"Admin\" -> \"Policies\" -> \"Add / update a policy\"，如下填写:\n* Name: ha_mirror_queue\n* Pattern: ^ \n* Apply to: All exchange and queues\n* Defination: \n * ha-mode:  all\n \n添加好后点击\"Add\"\n\n配置好后，我们尝试创建一个Queue，在Web管理工具点击\"Queues\" -> \"Add queue\"，name写test，其他保持默认，最后点击\"Add queue\"。\n\n添加完成后，可以发现队列有一个\"+2\"的标志，如下图所示。这个+2意思是队列有额外的2个备份(主1+镜2一共3个节点)，镜像配置成功。\n\n![查看RabbitMQ镜像队列](./rabbitmq-mirror-queue.png \"查看RabbitMQ镜像队列\")\n\n至此，我们已经完成了RabbitMQ的集群配置、镜像队列配置。\n\n关于Rabbit MQ的高可用、集群的更相信信息，可以查看官方文档:[RabbitMQ 集群](http://www.rabbitmq.com/clustering.html)。\n"
  },
  {
    "path": "legacy/ms-msgq/rocketmq-devops.md",
    "content": "# RocketMQ 消息队列的运维\n\n在前两节，我们讨论了RabbitMQ。\n\n然而根据实际的生产经验来看，当系统瞬时流量达到一定规模时，上述两款产品都不再适合作为消息系统的首选。\n\nRabbitMQ在企业级应用是没有问题的，但它的抗消息堆积能力非常差，一旦突发流量提高，发生事件堆积，整个RabbitMQ集群就会挂起拒绝接受新消息，在极端情况下，甚至会发生RabbitMQ集群的宕机和消息丢失。\n\nRocketMQ是一款高性能的分布式消息队列，它借鉴了Kafka的设计思路并继承了其高吞吐的特点，并重点改进了延迟，使得其更加适用于消息的实时处理。根据官方评测，在主流服务器上，RocketMQ的处理性能可达7万/秒，且随着Topic数量（队列数目）的增长而基本保持稳定，比Kafka更为稳定[^1]。\n\n在本小节，我们将讨论RocketMQ的运维，一般有两种方案。\n\n* RocketMQ部署在多台物理机上，优点是性能可靠。可以参考[官方集群部署文档](https://rocketmq.apache.org/docs/rmq-deployment/)。\n* RocketMQ部署在容器集群上，优点是运维方便。本小节将主要介绍这种方案。\n\nRocketMQ服务器有两种角色:\n* NameServer: 管理元数据和Broker服务器、客户端的连接入口\n* Broker: 处理消息队列的服务器\n\n在本小节，我们将构建4台服务器的RocketMQ集群，2台NameServer、2台Broker。\n\n首先构建4个PersistentVolume，给上述4台机器使用:\n```yaml\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: pv011 \nspec:\n  storageClassName: standard\n  accessModes:\n    - ReadWriteOnce\n  capacity:\n    storage: 20Gi\n  hostPath:\n    path: /data/pv011/\n---\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: pv012 \nspec:\n  storageClassName: standard\n  accessModes:\n    - ReadWriteOnce\n  capacity:\n    storage: 20Gi\n  hostPath:\n    path: /data/pv012/\n---\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: pv021\nspec:\n  storageClassName: standard\n  accessModes:\n    - ReadWriteOnce\n  capacity:\n    storage: 20Gi\n  hostPath:\n    path: /data/pv021/\n---\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: pv022\nspec:\n  storageClassName: standard\n  accessModes:\n    - ReadWriteOnce\n  capacity:\n    storage: 20Gi\n  hostPath:\n    path: /data/pv022/\n\n```\n\n应用4个持久化目录:\n```\nkubectl -f ./pvs.yaml\n```\n\n接着，看一下NameServer的部署:\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: rn \nspec:\n  ports:\n  - port: 9876\n  selector:\n    app: rocketmq-nameserver\n  clusterIP: None\n---\napiVersion: apps/v1\nkind: StatefulSet \nmetadata:\n  name: rocketmq-nameserver \nspec:\n  selector:\n    matchLabels:\n      app: rocketmq-nameserver\n  serviceName: \"rn\"\n  replicas: 2\n  template:\n    metadata:\n      labels:\n        app: rocketmq-nameserver \n    spec:\n      restartPolicy: Always\n      hostname: rocketmq-nameserver \n      containers:\n      - name: rocketmq-nameserver-ct\n        imagePullPolicy: Never\n        image: coder4/rocketmq:4.2.0 \n        ports:\n        - containerPort: 9876 \n        volumeMounts:\n        - mountPath: /opt/rocketmq_home\n          name: rocketmq-nameserver-pvc\n        env:\n        - name: NAME_SERVER\n          value: \"true\"\n  volumeClaimTemplates:\n  - metadata:\n      name: rocketmq-nameserver-pvc\n    spec:\n      storageClassName: standard\n      accessModes:\n        - ReadWriteOnce\n      resources:\n        requests:\n          storage: 20Gi\n\n```\n\n如上所示：\n* 我们使用了定制镜像coder4/rocketmq，它集成了NameServer/Broker并支持集群部署\n* 使用StatefulSet部署两台相互独立的NameServer，主机名分别为rocketmq-nameserver-0和rocketmq-nameserver-1\n* Volume的挂在点是/opt/rocketmq_home，其中会包含data和log两个子目录。\n\n启动一下，稍后成功：\n```yaml\nkubectl apply -f ./nameserver-service.yaml\n\nkubectl get pods\n\nNAME                                                   READY     STATUS    RESTARTS   AGE\nrocketmq-nameserver-0                                  1/1       Running   0          2m\nrocketmq-nameserver-1                                  1/1       Running   0          2m\n\n```\n\n接下来，我们看一下Broker。\n针对Broker，官方提供了几种高可用方案，我们这里采用\"two master no slave\"的模式，更多模式可参考官方文档。\n```yaml\npiVersion: v1\nkind: Service\nmetadata:\n  name: rb \nspec:\n  ports:\n  - name: p9\n    port: 10909 \n  - name: p11\n    port: 10911\n  selector:\n    app: rocketmq-brokerserver\n  clusterIP: None\n---\napiVersion: apps/v1\nkind: StatefulSet \nmetadata:\n  name: rocketmq-brokerserver \nspec:\n  selector:\n    matchLabels:\n      app: rocketmq-brokerserver\n  serviceName: \"rb\"\n  replicas: 2\n  template:\n    metadata:\n      labels:\n        app: rocketmq-brokerserver \n    spec:\n      restartPolicy: Always\n      hostname: rocketmq-brokerserver \n      containers:\n      - name: rocketmq-brokerserver-ct\n        imagePullPolicy: Never\n        image: rocketmq:latest\n        ports:\n        - containerPort: 10909 \n        - containerPort: 10911\n        volumeMounts:\n        - mountPath: /opt/rocketmq_home\n          name: rocketmq-brokerserver-pvc\n        env:\n        - name: NAME_SERVER_LIST\n          value: \"rocketmq-nameserver-0.rn:9876;rocketmq-nameserver-1.rn:9876\" \n        - name: BROKER_SERVER\n          value: \"true\"\n        - name: BROKER_CLUSTER_CONF\n          value: \"2m-noslave\"\n  volumeClaimTemplates:\n  - metadata:\n      name: rocketmq-brokerserver-pvc\n    spec:\n      storageClassName: standard\n      accessModes:\n        - ReadWriteOnce\n      resources:\n        requests:\n          storage: 20Gi\n\n```\n\n上述brokerserver的配置与nameserver存在如下区别：\n* 通过环境变量NAME_SERVER_LIST设定了nameServer的集群列表，即之前启动的两台机器。\n* BROKER_SERVER表示启用的是broker server模式\n* BROKER_CLUSTER_CONF表示集群配置模式，即我们提到的\"双主零从模式\"\n\n我们也启动一下broker server，稍等一会后，会成功：\n```yaml\nkubectl apply -f ./broker-service.yaml\n\nkubectl get pods\n\nNAME                                                   READY     STATUS    RESTARTS   AGE\nrocketmq-brokerserver-0                                1/1       Running   0          59s\nrocketmq-brokerserver-1                                0/1       Running   0          49s\n\n```\n\n我们尝试用RocketMQ的自带工具n查看一下broker集群状态，能发现两台机器，说明集群部署成功：\n```shell\n./mqadmin clusterList -n \"rocketmq-nameserver-0.rn:9876;rocketmq-nameserver-1.rn:9876\"        \n\n#Cluster Name     #Broker Name            #BID  #Addr                  #Version                #InTPS(LOAD)       #OutTPS(LOAD) #PCWait(ms) #Hour #SPACE\nDefaultCluster    broker-a                0     172.17.0.15:10911      V4_2_0_SNAPSHOT          0.00(0,0ms)         0.00(0,0ms)          0 425363.14 -1.0000\nDefaultCluster    broker-b                0     172.17.0.16:10911      V4_2_0_SNAPSHOT          0.00(0,0ms)         0.00(0,0ms)          0 425363.14 -1.0000\n\n```\n\n至此，我们完成了RocketMQ的集群部署工作。\n\n[^1]: [Kafka vs. RocketMQ- Multiple Topic Stress Test Results](https://medium.com/@Alibaba_Cloud/kafka-vs-rocketmq-multiple-topic-stress-test-results-d27b8cbb360f)\n\n"
  },
  {
    "path": "legacy/ms-msgq/sb-kafka.md",
    "content": "# Spring Boot整合Kafka\n\n"
  },
  {
    "path": "legacy/ms-msgq/sb-rabitmq.md",
    "content": "# Spring Boot整合RabbitMQ\n\n在上一节，我们已经掌握了RabbitMQ集群的运维方法。\n\n在本章中，我们来看一下如何在Spring Boot中集成RabbitMQ\n\n## 依赖配置\nRabbitMQ实现了AMQP协议，因此，在Spring Boot中，我们直接引入ampq的starter\n```groovy\ncompile(\"org.springframework.boot:spring-boot-starter-amqp\")\n```\n\n## RabbitMQ服务配置\n在yaml中，我们需要配置RabbitMQ服务的地址:\n```yaml\nspring.rabbitmq:\n  addresses: rmq1:5672,rmq2:5672\n  username: guest\n  password: guest\n```\n## 发送消息\n在Spring Boot中发送消息需要4个步骤:\n1. 声明Exchange\n1. 声明Queue\n1. 声明Queue到Exchange的绑定\n1. 调用RabbitTemplate发送消息\n\n前三步声明都可以通过Spring Boot注入:\n```java\n#Bean\npublic TopicExchange createExchange() {\n    return new TopicExchange(\"exchange\");\n}\n \n@Bean\npublic Queue createQueue() {\n    return new Queue(\"queue\");\n}\n \n \n@Bean\npublic Binding declareBindingGeneric() {\n    return BindingBuilder.bind(createQueue()).to(createExchange()).with(\"#\");\n}\n\n```\n\n如上所示，分别声明了Exchange、Queue和Binding\n\n发送消息相对比较简单，拿到注入的RabbitTemplate后，直接发送即可。\n\n```java\npublic class Tut1Sender {\n\n    @Autowired\n    private RabbitTemplate template;\n\n    @Autowired\n    private Queue queue;\n\n    @Scheduled(fixedDelay = 1000, initialDelay = 500)\n    public void send() {\n        String message = \"Hello World!\";\n        this.template.convertAndSend(queue.getName(), message);\n        System.out.println(\" [x] Sent '\" + message + \"'\");\n    }\n}\n\n\n```\n\n如上，我们直接调用convertAndSend即可发送字符串类型的消息。如果想发送更复杂类型的，可以让类型实现Converter。\n\n## 接受消息\n\n接受消息，直接绑定Queue的名字即可:\n```java\n@RabbitListener(queues = \"queue\")\npublic class Tut1Receiver {\n\n    @RabbitHandler\n    public void receive(String in) {\n        System.out.println(\" [x] Received '\" + in + \"'\");\n    }\n}\n```\n\n至此，我们可以在Sping Boot中集成RabbitMQ了。\n"
  },
  {
    "path": "legacy/ms-msgq/sb-rocketmq.md",
    "content": "# Spring Boot整合RocketMQ\n\n在本小节中，我们将讨论在Spring Boot中整合RocketMQ。\n\n我们选用官方推荐的Spring Boot拓展[rocketmq-spring-boot-starter](https://github.com/apache/rocketmq-externals/tree/master/rocketmq-spring-boot-starter)\n\n## 依赖接入\n\n首先，需要在Spring Boot中添加依赖:\n```groovy\ncompile 'com.qianmi:spring-boot-starter-rocketmq:1.1.0-RELEASE'\n```\n\n## 配置RocketMQ的服务器\n\n在配置中，我们需要设定RocketMQ的服务器集群\n\n```yaml\nspring.rocketmq:\n    nameServer: 127.0.0.1:9876\n    producer.group: lmsia-abc\n```\n\n如上所示：\n* spring.rocketmq.nameServer 配置元服务器的地址\n* spring.rocketmq.producer.group 是[Producer Group](http://rocketmq.apache.org/docs/core-concept/)。我们这里设定为微服务的名字，即相同的微服务都认为是同一个Producer Group。\n\n## 在Spring Boot中首发消息\n\n按照上述进行配置后，自动配置会被激活，并自动注入RocketMQTemplate用于生成发消息、ListenerContainer用于收消息。\n\n上述对使用方都是透明的，在Spring Boot中收发消息非常简单，如下：\n\n```java\n@Service\n@RocketMQMessageListener(topic = TOCPIC, consumerGroup = LmsiaAbcConstant.PROJECT_NAME)\npublic class MyEventHandlerImpl implements MyEventHandler, RocketMQListener<MyEvent> {\n\n    private Logger LOG = LoggerFactory.getLogger(getClass());\n\n    @Resource\n    private RocketMQTemplate rocketMQTemplate;\n\n    @Override\n    public void send(MyEvent event) {\n        rocketMQTemplate.convertAndSend(TOCPIC, event);\n    }\n\n    @Override\n    public void onMessage(MyEvent message) {\n        LOG.info(\"receive message, data = {}\", message.getData());\n    }\n}\n```\n\n如上所述:\n* topic需要配置成一致的，即TOPIC常量，类似的，consumerGroup定义为微服务名称，即认为同一个微服务的所有节点属于同一个组。\n* 我们自动注入了RocketMQTemplate用于发消息，消息默认继承自Serializable\n* RocketMQListener会在收到消息后回调onMessage方法。\n\n如果你使用过RocketMQ的官方客户端的话，会发现其易用性要远低于[spring-boot-starter-rocketmq](https://github.com/apache/rocketmq-externals/tree/master/rocketmq-spring-boot-starter)。\n\nRocketMQ还支持顺序消息、广播消息等更多功能，spring-boot-starter-rocketmq中都支持了具体的实现，可以直接参考上述项目主页的说明。\n"
  },
  {
    "path": "legacy/ms-storage/README.md",
    "content": "# 微服务的存储与缓存\n\n在计算机领域，有一句人尽皆知的名言\"算法=程序+数据结构\"，这是图灵奖获得者尼古拉斯·沃斯提出的[^1]。\n\n从大师的这句名言中，我们不难感受到，数据的存储方式与算法同等重要。\n\n在微服务架构中，我们虽然不会研究特定的算法，但数据的存储依然是必不可少的环节。\n\n例如：用户的注册信息、订单信息、生成的UGC内容等，都需要以合适的方式存储下来。\n\n存下来只是第一步，更为关键的是，我们需要在需要时，以合理的速度、合理的成本开销将数据读取出来。特别是在互联网软件开发中，数据经常是\"读多写少\"，数据的读取往往比存储更加关键。\n\n请注意我的用词\"合理的速度\"、\"合理的成本\"。为了理解这两点，我们先了解一下常见的存储方式:\n\n\n\n\n请注意我的用词，是\"合理的速度\"、\"合理的成本\"，并非\"最快的速度\"、\"最低的成本\"。\n\n\n\n\n\n\n[^1](https://www.jianshu.com/p/19f68c72effc)\n\n"
  },
  {
    "path": "legacy/ms-storage/memcached-devops.md",
    "content": "# Memcached 缓存服务的运维 \n\n如果业务进一步发展，通过\"读写分离\"、\"分库分表\"后，数据库的性能依然无法满足高并发读请求，此时就需要缓存出马了。\n\n缓存的原理其实非常简单: 用\"存取速度更快的空间\"换取\"存取速度更慢的时间\"。\n\n当然，天下没有免费的午餐，缓存自然也是有代价的。单位容量的内存比磁盘要昂的多。幸运的是，根据数据的局部性原理[^2]，我们可以有如下策略:\n* 只选择少量的\"热数据\"放入缓存\n* 当缓存空间不够放置\"热数据\"时，根据策略替换掉缓存中的已有数据。\n\n本节的主角是Memcached，一款高性能的内存缓存，性能可达每秒5万次[^1]。\n\nMemcached本身是不支持集群的，但可以通常可以部署多台服务。在访问时，可以根据key的哈希值取模进行分片，然后访问不同的Memcached结点。\n\n## 集群搭建\n由于Memcached是全内存的，所以无需创建Volume挂载点。\n\n在这里，我们没有使用Deployment，而是使用了[StatefulSet](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/)。\n\nStatefulSet与Deployment基本相同，唯一的的区别是，前者认为所有副本是相互独立的，而后者认为所有副本是互为冗余的。\n\n对于微服务的应用场景，每个节点都是完全相同的逻辑、连接完全相同的数据库、执行等同的操作，所以我们用的一直是Deployment。\n\n而对于Memcached，我们会将不同数据分片到不同Memcached结点上，他们是相互独立而不是可替代的，所以我们采用了StatefulSet。\n\nmemcached-service.yaml:\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: memcached\nspec:\n  ports:\n  - port: 11211\n  selector:\n    app: memcached\n  clusterIP: None\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n  name: memcached\nspec:\n  selector:\n    matchLabels:\n      app: memcached\n  serviceName: \"memcached\"\n  replicas: 2\n  template:\n    metadata:\n      labels:\n        app: memcached\n    spec:\n      restartPolicy: Always\n      hostname: memcached\n      containers:\n      - name: memcached-ct\n        image: memcached:1.5-alpine\n        ports:\n        - containerPort: 11211\n        args: [\"memcached\", \"-m\", \"256\"]\n\n```\n\n简单说明下:\n* 我们声明了StatefulSet为memcached，2个独立节点\n* 限定了内存使用为256m\n\n启动下:\n```shell\nkubectl apply -f memcached-service.yaml\n```\n\n然后我们登录一个额外的docker上，尝试ping一下，是可以的，说明启动成功:\n```shell\n\nping memcached-0.memcached\nPING memcached1 (172.17.0.8): 56 data bytes\n64 bytes from 172.17.0.8: seq=0 ttl=64 time=1.014 ms\n64 bytes from 172.17.0.8: seq=1 ttl=64 time=0.138 ms\n64 bytes from 172.17.0.8: seq=2 ttl=64 time=0.134 ms\n\n\nping memcached-1.memcached\nPING memcached2 (172.17.0.9): 56 data bytes\n64 bytes from 172.17.0.9: seq=0 ttl=64 time=0.076 ms\n64 bytes from 172.17.0.9: seq=1 ttl=64 time=0.123 ms\n64 bytes from 172.17.0.9: seq=2 ttl=64 time=0.119 ms\n\n```\n\n注意上面对不同节点的DNS域名为\"statefulName-x\".\"serviceName\"\n\nMemcached的配置看起来很简单，但是分片策略还需要进一步思考。\n\n例如，前面提到了利用哈希值取模，可以实现Memcahced在客户端的分片。按照此策略，如果现在要增加一台机器到3台，计算取模的值将发生变化，缓存上的所有的数据都需要清空重新分片。\n\n这种\"推倒重来\"的策略看起来简单，但可能会导致缓存击穿甚至造成线上故障。\n\n想解决这类问题，可以采用[一致性哈系](http://www.cnblogs.com/RockLi/p/3530176.html)，它可以尽可能地减少机器变动后，造成的数据重分布。\n\nMemcached的日常运维比较简单，常见的操作就是清空全部缓存，可以通过nc指令来完成:\n\n```shell\necho 'flush_all' | nc memcached1 11211\n\nOK\n```\n\n提醒一下，线上执行清空操作要非常谨慎，若系统性能严重依赖缓存，那么清空操作往往会导致缓存击穿并造成系统故障。\n\n## 小结\n\n本节，我们使用StatefulSet，完成了Memcached集群的运维，并介绍了Memcahced集群运维中常见的问题。\n\n[^1]: [Memcached性能评测数据](https://github.com/scylladb/seastar/wiki/Memcached-Benchmark)\n[^2]: 分为空间局部性和时间局部性，可参考[局部性原理浅析](https://www.cnblogs.com/yanlingyin/archive/2012/02/11/2347116.html)\n"
  },
  {
    "path": "legacy/ms-storage/mysql-devops.md",
    "content": "# MySQL数据库的运维\n\n近几年，以\"Redis\"、\"MongoDB\"为代表的\"NoSQL\"数据库迅速崛起。\"NoSQL\"并不是\"没有SQL\"而是\"Not Only SQL\"。在特定场景下，NoSQL数据库确实解决了一些问题，例如：\n\n* 海量数据的列存储: 以HBase、Cassandra。\n* 速度更快的内存数据库：Redis。\n* 文档存储：Mongo DB。\n* 支持全文检索特性: ElasticSearch。\n\n若将视角放到更普适的场景，关系型数据库凭借着\"ACID\"特性，依然牢牢占据着数据存储的核心地位。\n\n根据市场调研显示，在数据库领域，排名前3的分别是：\n1. Oracle\n1. MySQL\n1. Microsoft SQL Server\n\n其中，MySQL是最流行的开源关系数据库，其性能较为优秀、服务稳定、社区活跃，是众多中小型公司的首选。\n\n在本节中，我们首先将讨论MySQL数据库的基础运维，随后讨论常见的优化手段－MySQL主从复制。\n\n## MySQL数据库的部署\n\n我们的MySQL依然部署在Kubernetes上，首先创建挂载点：\n```shell\nsudo mkdir /data/mysql\n\nsudo chmod -R 777 /data/mysql\n\n```\n\n如果是线上环境，可以将挂载点设定在SSD磁盘等IOPS较高的存储介质上。\n\n看一下部署脚本:\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: mysql\nspec:\n  ports:\n  - port: 3306\n  selector:\n    app: mysql\n  clusterIP: None\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: mysql\nspec:\n  selector:\n    matchLabels:\n      app: mysql\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        app: mysql\n    spec:\n      nodeSelector:\n        kubernetes.io/hostname: minikube\n      restartPolicy: Always\n      hostname: mysql\n      containers:\n      - name: mysql-ct\n        image: mysql:8\n        ports:\n        - containerPort: 3306\n        volumeMounts:\n        - mountPath: \"/var/lib/mysql\"\n          name: volume\n        env:\n        - name: \"MYSQL_ROOT_PASSWORD\"\n          value: \"root123\"\n      volumes:\n      - name: volume\n        hostPath:\n          path: /data/mysql/\n\n```\n\n上面的部署yaml与之前的类似，只解释几点:\n* 上述实际启动了一个\"Headless\"Service，即将clusterIP设置为None。此时将不再启动单独的VIP，而是让dns直接解析到Pod的IP上\n* MySQL的存储数据量较大，一般固定在某台物理机上，不会主动迁移。例如这里我们选择了minikube。\n* MYSQL_ROOT_PASSWORD是root的管理员密码，但root默认只允许本机登录。\n\n我们来尝试登录下，首先获取docker的容器id:\n```shell\nkubectl get pods\n\nNAME                                                READY     STATUS    RESTARTS   AGE\nmysql-7bf88bcfd-gljv7                               1/1       Running   1          4m\n\nkubectl describe pod mysql-7bf88bcfd-gljv7 \n\n...\nContainer ID:   docker://479a3b1d9e7b9f445f2cb8133e156de480337c23c6f27aa541ca4df8b3cf944d\n...\n```\n\n然后尝试登录MySQL，是成功的:\n```shell\nminikube ssh\n\n$docker exec -i -t 479a3b1d9e7b9f445f2cb8133e156de480337c23c6f27aa541ca4df8b3cf944d /bin/sh\n\n$#mysql -h localhost -u root -proot123\n\nmysql -h localhost -uroot -proot123\n\nmysql: [Warning] Using a password on the command line interface can be insecure.\nWelcome to the MySQL monitor.  Commands end with ; or \\g.\nYour MySQL connection id is 8\nServer version: 8.0.11 MySQL Community Server - GPL\n\nCopyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.\n\nOracle is a registered trademark of Oracle Corporation and/or its\naffiliates. Other names may be trademarks of their respective\nowners.\n\nType 'help;' or '\\h' for help. Type '\\c' to clear the current input statement.\n\nmysql> \n\n```\n\n至此，我们已经完成了MySQL数据库的基本搭建。\n\n## MySQL数据库的日常运维\n\n出于性能、安全性、可拓展性等考量，一般会为MySQL服务器建立多个库，并分配不同的帐号。在微服务架构下，不同的微服务应使用不同的库，尽量避免数据库层的耦合。\n\n因此，新建数据库、分配帐号是MySQL日常运维工作最常见的事情。我们给出一个脚本：\n\n```shell\nif [ $# -ne 1 ]; then\n    echo \"Usage $0 <dbname>\"\n    exit -1\nfi\n\nDB_NAME=$1\nDB_USER=\"lmsia\"\nDB_PASS=\"pass\"\n\necho \"CREATE DATABASE IF NOT EXISTS $DB_NAME DEFAULT CHARSET utf8mb4;\"\necho \"CREATE USER $DB_USER IDENTIFIED by '$DB_PASS';\"\necho \"GRANT ALL PRIVILEGES ON $DB_NAME.* to $DB_USER;\"\necho \"FLUSH PRIVILEGES;\"\n\n```\n\n在上述脚本中，会自动生成如下语句:\n* 建库(编码utf8mb4)\n* 建用户，并分配到上述库\n\n注意:脚本里的密码给的是固定的\"pass\"，在实际生产环境，应当使用随机密码。\n\n我们可以执行上述脚本，然后将生成的语句粘帖到root登录的mysql客户端，即可完成数据库的添加工作。\n\n## MySQL数据库的同步\n\nMySQL较为优秀，但也不是万能的银弹。随着业务逐步发展壮大，MySQL的性能可能会成为瓶颈，例如:\n* CPU占用持续较高\n* 网络带宽常常打满\n* 查询慢\n\n当性能成为瓶颈时，应首先从使用方面查找原因，举几个常见例子:\n* 查询语句是否合理利用了索引\n* 事务锁表时间是否太长\n* 是否经常性的超大批量数据导出占用了带宽\n\n如果使用上都没有问题，依然出现性能问题，可以考虑从架构上优化，常见的手段有:\n* 读写分离: 如果读多写少，可以MySQL端进行主从复制，微服务中读写分离。\n* 分库: 某个库占用大量性能，可考虑将其拆分到单独的MySQL服务器上\n* 分表: 单表行数超过1000万后，可考虑将表进行水平、垂直划分，拆分到不同MySQL服务器上。\n\n其中分库和分表一般要借助中间件实现，在本小节，我们先介绍第一种手段：读写分离。\n\n读写分离也是一种基于特定场景的优化。在互联网软件开发中，经常会有\"读多写少\"的情景，举几个例子:\n* 微博上看的人多，发微博的人少。\n* 新闻浏览的人数多，评论的人数少，发布的新闻更少。\n\n针对\"读多写少\"，我们可以将读和写分离开，使得读性能得到更高的保证，看一下读写分离的原理:\n\n![MySQL读写分离](./mysql-replication.png \"MySQL读写分离\")\n\n如图所示，我们开启两个MySQL服务器:\n* Master，MySQL数据库服务器，主要承担写请求\n* Slave，MySQL数据库服务器，负责承担读请求\n\n要说明的是：写SQL到达Master后，会通过住从复制机制(Replication)同步到Slave上，这个过程是异步的，会存在延迟。若微服务的读请求都发往Slave，那么势必也会受到住从复制的延迟影响，特别是对于\"写后读\"这个场景，可能会导致一些Bug，感兴趣的朋友自行思考如何解决。\n\nMySQL的主从数据同步有几个要点:\n* master和slave机器配置了不同的server-id\n* slave机器通过配置或sql命令设定master的主机名\n\n下面我们尝试在Kubernetes中配置一组主从复制的MySQL服务器。\n\n首先创建master(写库)和slave(读库)的挂载点:\n```shell\nsudo mkdir /data/mysql_writer\nsudo mkdir /data/mysql_reader\n\nsudo chmod -R 777 /data/mysql_writer\nsudo chmod -R 777 /data/mysql_reader\n```\n\n然后看一下master(写服务)的定义, mysql-writer-service.yaml:\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: mysql-writer\nspec:\n  ports:\n  - port: 3306\n  selector:\n    app: mysql-writer\n  clusterIP: None\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: mysql-writer\nspec:\n  selector:\n    matchLabels:\n      app: mysql-writer\n  template:\n    metadata:\n      labels:\n        app: mysql-writer\n    spec:\n      nodeSelector:\n        kubernetes.io/hostname: minikube\n      restartPolicy: Always\n      hostname: mysql-writer\n      containers:\n      - name: mysql-writer-ct\n        image: coder4/mysql-replication:8.0\n        ports:\n        - containerPort: 3306\n        volumeMounts:\n        - mountPath: \"/var/lib/mysql\"\n          name: volume\n        env:\n        - name: \"MYSQL_ROOT_PASSWORD\"\n          value: \"root123\"\n        args: [\"--server-id=1\"]\n      volumes:\n      - name: volume\n        hostPath:\n          path: /data/mysql_writer/\n````\n\n如上所述，配置基本与之前的单机MySQL类似，区别是:\n* 使用了一个支持主从同步的镜像，coder4/mysql-replication\n* 主机名、dns配置为mysql-writer\n* 服务id是1\n\n应用一下，过一会可以看到启动成功:\n```shell\nkubectl apply -f ./mysql-writer-service.yaml\n```\n\n下面看下读库(slave结点), mysql-reader-service.yaml:\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: mysql-reader\nspec:\n  ports:\n  - port: 3306\n  selector:\n    app: mysql-reader\n  clusterIP: None\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: mysql-reader\nspec:\n  selector:\n    matchLabels:\n      app: mysql-reader\n  template:\n    metadata:\n      labels:\n        app: mysql-reader\n    spec:\n      nodeSelector:\n        kubernetes.io/hostname: minikube\n      restartPolicy: Always\n      hostname: mysql-reader\n      containers:\n      - name: mysql-reader-ct\n        image: coder4/mysql-replication:8.0\n        ports:\n        - containerPort: 3306\n        volumeMounts:\n        - mountPath: \"/var/lib/mysql\"\n          name: volume\n        env:\n        - name: \"MYSQL_ROOT_PASSWORD\"\n          value: \"root123\"\n        - name: \"MYSQL_MASTER_SERVER\"\n          value: \"mysql-writer\"\n        args: [\"--read-only=1\", \"--server-id=2\"]\n      volumes:\n      - name: volume\n        hostPath:\n          path: /data/mysql_reader/\n```\n\n与独立MySQL服务器的配置相比，主要做了如下修改：\n* 设置主结点（写库）为mysql-writer\n* 用于主从同步的用户名密码同写库\n* 只读，这主要是为了防止误删除数据，导致主从不一致\n* 服务id是2\n\n类似的，我们启动下从库:\n```shell\nkubectl apply -f ./mysql-reader-service.yaml\n```\n\n下面我们尝试在主库创建数据库，看看能否自动同步到从库:\n\n(这里省略登录mysql-writer的过程，具体参照本节第一部分)\n```sql\nCREATE DATABASE IF NOT EXISTS lmsia_user DEFAULT CHARSET utf8mb4;\nCREATE USER lmsia IDENTIFIED by 'pass';\nGRANT ALL PRIVILEGES ON lmsia_user.* to lmsia;\nFLUSH PRIVILEGES;\n```\n\n然后登录mysql-reader看一下，发现可以成功登录，说明主从同步的配置是成功的:\n```shell\nmysql -hlocalhost -ulmsia -ppass lmsia_user\nmysql: [Warning] Using a password on the command line interface can be insecure.\nWelcome to the MySQL monitor.  Commands end with ; or \\g.\nYour MySQL connection id is 6\nServer version: 5.7.14-log MySQL Community Server (GPL)\n\nCopyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved.\n\nOracle is a registered trademark of Oracle Corporation and/or its\naffiliates. Other names may be trademarks of their respective\nowners.\n\nType 'help;' or '\\h' for help. Type '\\c' to clear the current input statement.\n\nmysql>\n\n```\n\n上述例子只展示了coder4/mysql-replication镜像的部分配置，如果你想启用更多高级配置，可以参考[docker-mysql-replication](https://github.com/liheyuan/docker-mysql-replication)\n\nMySQL主从同步是一个很有意思的话题，例如:\n* 上述例子中，mysql-reader只会同步启动后接受到的binlog，如何同步启动前mysql-writer的改动呢?\n* 本小节一开始提到的\"写后读\"问题，能不能通过\"写到slave后才算写完成\"的方式解决呢?\n* 能否配置多个slave节点呢?\n\n由于篇幅所限，这里不会对上述问题展开讨论，如果你感兴趣，可以在[MySQL Replication官方文档](https://dev.mysql.com/doc/refman/8.0/en/replication.html)中找到答案。\n\n"
  },
  {
    "path": "legacy/ms-storage/redis-devops.md",
    "content": "# Redis 数据库的运维\n\n作为纯内存缓存，Memcached拥有非常出色的读写性能，但也存在一个较为严重的缺点：无法持久化。\n\n这意味着，一旦Memcached服务重启(更常见的是掉电)，之前所有的缓存就会丢失。若线上的流量很大，这种重启很容易诱发\"缓存雪崩\"，从而导致系统故障。\n\nRedis的出现很好的解决了这个问题，它是一款高性能的内存的数据库，既不仅数据的支持持久化、也内置了许多数据结构，方便实现各种需求。在一些场景下[^1]，可以直接用Redis取代Memcached + MySQL的组合。\n\n本节将讨论Redis运维相关的问题。\n\n## Redis单服务器的运维\n\nRedis同时支持单服务器、高可用、集群等三种方案。\n\n我们先来看一下单服务器方案。\n\n顾名思义，单服务器模式下，只启动一个Redis服务进程，若服务挂掉则Redis不可用。可见，这种方案并不保证高可用。\n\n与之前的部署类似，我们同样将Redis部署在Kubernetes集群上，首先是创建Volume挂载点\n\n```shell\nsudo mkdir /data/redis\n\nsudo chmod -R 777 /data/redis\n\n```\n\n接着，我们看一下部署文件:\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: redis\nspec:\n  ports:\n  - port: 6379\n  selector:\n    app: redis\n  clusterIP: None\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: redis\nspec:\n  selector:\n    matchLabels:\n      app: redis\n  template:\n    metadata:\n      labels:\n        app: redis\n    spec:\n      nodeSelector:\n        kubernetes.io/hostname: minikube\n      restartPolicy: Always\n      hostname: redis\n      containers:\n      - name: redis-ct\n        image: redis:3.2-alpine\n        ports:\n        - containerPort: 6379\n          hostPort: 6379\n        volumeMounts:\n        - mountPath: \"/data\"\n          name: volume\n      volumes:\n      - name: volume\n        hostPath:\n          path: /data/redis/\n\n```\n\n简要说明下：\n* 这里使用Redis官方的Docker镜像\n* 与MySQL类似，考虑到持久化后的数据量可能较大，我们将Pod绑定到minikube机器上，以固定存储。\n\n应用servie，稍等一会，成功:\n```yaml\nkubectl apply -f kubectl describe pod redis-798659bc79-vdht7\n\nservice \"redis\" created\ndeployment.apps \"redis\" created\n\n```\n\n我们尝试连接一下，首先获取Pod的ContainerId:\n```shell\n\nkubectl get pods\nNAME                                                READY     STATUS    RESTARTS   AGE\nredis-798659bc79-vdht7                              1/1       Running   0          4m\n\nkubectl describe pod redis-798659bc79-vdht7\n\n...\n    Container ID:   docker://090c2a7a004200aa6f0c4f3779e3823c401f03ad4f23985fdc08c38f86d6c598\n...\n\n\n```\n\n尝试登录，并登录redis服务器:\n```shell\nminikube ssh\n$ docker exec -i -t 090c2a7a004200aa6f0c4f3779e3823c401f03ad4f23985fdc08c38f86d6c598 /bin/sh\n\n/data # echo \"info\" | redis-cli \n# Server\nredis_version:3.2.12\nredis_git_sha1:00000000\n....\n\n```\n\n通过上面的操作，可以成功登录Redis服务器，并获取了版本信息。\n\n至此，Redis的单服务器模式配置完成。\n\n## Redis高可用(Sentinel)集群的运维\n\n在上面的Redis单服务器模式下，存在单点故障，假如这个Redis进程挂掉了，则Redis就无法提供服务了。\n\n为了解决可用性，Redis内置了两种高可用方案，较为经典的是Sentinel集群。\nSentinel集群采用主备模式：\n* 支持多个Redis服务组，不同服务组通过唯一的master_name标识。\n* 组内一个主redis节点提供服务，若干从redis节点定期从主redis节点同步数据。但从节点只作为热备，不提供服务。\n* 当某个组的主节点挂掉后，Sentinel服务会检测到主节点故障，并进行主备切换。\n* 客户端先连接Sentinel，根据master_name获取组内主Redis节点的IP和端口信息，再连接。\n\n![Sentinel集群架构](./redis-sentinel.png \"Sentinel架构\")\n\n如果你对Sentinel的架构细节感兴趣，可以阅读[官方文档](https://redis.io/topics/sentinel)。\n\n首先，我们来部署一组Redis服务的主节点:\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: redis-lmsia-test1-master\nspec:\n  ports:\n  - port: 6379\n  selector:\n    app: redis-lmsia-test1-master\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: redis-lmaia-test1-deployment\nspec:\n  selector:\n    matchLabels:\n      app: redis-lmsia-test1-master\n  template:\n    metadata:\n      labels:\n        app: redis-lmsia-test1-master\n    spec:\n      nodeSelector:\n        kubernetes.io/hostname: minikube\n      restartPolicy: Always\n      hostname: redis\n      containers:\n      - name: redis-sentinel-ct\n        image: coder4/redis-sentinel-k8s:4.0.10\n        ports:\n        - containerPort: 6379\n        env:\n        - name: \"MASTER\"\n          value: \"true\"\n        - name: \"MASTER_NAME\"\n          value: \"lmsia_test1\"\n```\n\n如上所示:\n* 我们使用了自定制的镜像redis-sentinel-k8s，其原理可以查看项目主页[docker-redis-sentinel-k8s](https://github.com/liheyuan/docker-redis-sentinel-k8s)\n* MASTER=true，开启主节点模式\n* MASTER_NAME=lmsia_test1，Redis服务的组名叫lmsia_test1\n* 服务组名是redis-lmsia-test1-master，这个很重要，slave节点和sentinel会根据这个来定位master节点。\n\n接着，我们启动lmsia_test1这个服务组一个从节点：\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: redis-lmsia-test1-slave\nspec:\n  ports:\n  - port: 6379\n  selector:\n    app: redis-lmsia-test1-slave\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: redis-lmaia-test1-deployment\nspec:\n  selector:\n    matchLabels:\n      app: redis-lmsia-test1-slave\n  template:\n    metadata:\n      labels:\n        app: redis-lmsia-test1-slave\n    spec:\n      nodeSelector:\n        kubernetes.io/hostname: minikube\n      restartPolicy: Always\n      hostname: redis\n      containers:\n      - name: redis-sentinel-ct\n        image: coder4/redis-sentinel-k8s:4.0.10\n        ports:\n        - containerPort: 6379\n        env:\n        - name: \"SLAVE\"\n          value: \"true\"\n        - name: \"MASTER_NAME\"\n          value: \"lmsia_test1\"\n```\n\n如上，组内从节点的启动方式和主节点基本一致，有几个需要特别注意的：\n* MASTER_NAME需要和主节点保持一致，即lmsia_test1\n* SLAVE=true，开启从节点模式。\n\n我们先来启动这一组主从服务：\n```shell\nkubectl apply -f ./redis-lmsia-test1-master-service.yaml\nkubectl apply -f ./redis-lmsia-test1-slave-service.yaml\n```\n\n我们分别登录redis，看看他们的组状态，首先是master，身份是主节点，并可以看到从节点的IP:\n```shell\nredis-cli>info replication\ninfo replication\n# Replication\nrole:master\nconnected_slaves:1\nslave0:ip=172.17.0.8,port=6379,state=online,offset=168,lag=1\nmaster_replid:9b7dfe0b5f8d538d0f7b81d4095c239f1da72553\nmaster_replid2:0000000000000000000000000000000000000000\nmaster_repl_offset:168\nsecond_repl_offset:-1\nrepl_backlog_active:1\nrepl_backlog_size:1048576\nrepl_backlog_first_byte_offset:1\nrepl_backlog_histlen:168\n\n```\n\n然后slave，状态是从节点，可以看到主节点的IP:\n```shell\n# Replication\nrole:slave\nmaster_host:10.105.12.178\nmaster_port:6379\nmaster_link_status:up\nmaster_last_io_seconds_ago:2\nmaster_sync_in_progress:0\nslave_repl_offset:266\nslave_priority:100\nslave_read_only:1\nconnected_slaves:0\nmaster_replid:9b7dfe0b5f8d538d0f7b81d4095c239f1da72553\nmaster_replid2:0000000000000000000000000000000000000000\nmaster_repl_offset:266\nsecond_repl_offset:-1\nrepl_backlog_active:1\nrepl_backlog_size:1048576\nrepl_backlog_first_byte_offset:1\nrepl_backlog_histlen:266\n\n```\n\n接着，我们来启动Sentinel服务：\n```yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: redis-sentinel\nspec:\n  ports:\n  - port: 26379\n  selector:\n    app: redis-sentinel\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: redis-sentinel-deployment\nspec:\n  selector:\n    matchLabels:\n      app: redis-sentinel\n  replicas: 3\n  template:\n    metadata:\n      labels:\n        app: redis-sentinel\n    spec:\n      nodeSelector:\n        kubernetes.io/hostname: minikube\n      restartPolicy: Always\n      hostname: redis\n      containers:\n      - name: redis-sentinel-ct\n        image: coder4/redis-sentinel-k8s:4.0.10\n        ports:\n        - containerPort: 26379\n        env:\n        - name: \"SENTINEL\"\n          value: \"true\"\n        - name: \"MASTER_NAME_LIST\"\n          value: \"lmsia_test1\"\n\n```\n\n如上，我们部署了3个节点的Sentinel服务:\n* 使用我们定制的镜像redis-sentinel-k8s，其原理可以查看项目主页[docker-redis-sentinel-k8s](https://github.com/liheyuan/docker-redis-sentinel-k8s)\n* 打开26379端口，这是默认Sentinel的默认端口\n* 环境变量SENTINEL表明以Sentinel模式启动\n* 环境变量MASTER_NAME_LIST，列出了所有要监听的组名即master_name，用空格分割开。\n\n我们尝试连接任意一台sentinel来获取主结点的信息:\n```\nredis-cli -h localhost -p 26379\n> SENTINEL get-master-addr-by-name lmsia_test1\n1) \"10.105.12.178\"\n2) \"6379\"\n```\n\n组内主redis服务获取成功。\n\n至此，我们已经完成了Redis的Sentinel部署方式。\n\n## 小结\n在本节中，我们从讨论了Redis的优点，以及单服务的运维方式。\n\n接着，我们讨论了一种高可用Redis运维方案：Sentinel集群。这种方案可以保证Redis服务的高可用。但该方案也有明显的缺点：主备模式决定了资源的利用率只有50%，造成了一定的浪费。\n\n## 拓展阅读\n\n1. 在小结中，我们提到了Sentinel模式会造成一定的资源浪费。可以采用[Redis Cluster](https://redis.io/topics/cluster-tutorial)的部署模式，在保证高可用的同时，资源利用率。\n1. 为了保证高性能，Redis采用异步持久话的方式，分为rdb和aof两种，需要根据实际情况，选择适合的一种甚至混合方案。具体可参见文档（https://redis.io/topics/persistence）\n1. 若采用aof方式，积累较多修改后，重启redis会非常慢，可以定期进行[aof rewrite](https://redis.io/commands/bgrewriteaof)压缩aof日志。\n\n[^1]: 若要维持较高性能，建议保留足够的内存以存储全部数据。\n\n"
  },
  {
    "path": "legacy/ms-storage/sb-memcached.md",
    "content": "# Spring Boot整合Memcached\n\n前面已经提到，缓存是快速提升系统性能，缓解瓶颈的有效手段。\n\n缓存的种类多种多样，小到CPU的缓存，大到静态生成的页面缓存。在本小节中，我们主要讨论在Spring Boot中整合如下两种缓存:\n* 本地缓存: 在内存中开辟一小块空间，用于缓存，速度很快，但容量受限。我们采用Gruva中的缓存实现。\n* 网络缓存: 同一微服务的不同节点间，通过网络共享，例如Memcached。\n\n## 通用缓存接口\n\n既然要在微服务中支持2种缓存，不妨设计一个较为通用的接口:\n```java\npublic interface ICache<K, V> {\n\n    @Nullable\n    V get(K key);\n\n    @Nullable\n    default V cacheGet(K key, Function<K, V> func, int ttlSecs) {\n        V val = get(key);\n        if (val != null) {\n            return val;\n        } else {\n            val = func.apply(key);\n            put(key, val, ttlSecs);\n            return val;\n        }\n    }\n\n    default V cacheGet(K key, Function<K, V> func) {\n        return cacheGet(key, func, 0);\n    }\n\n    Map<K, V> batchGet(Collection<K> keys);\n\n    default Map<K, V> batchCacheGet(Collection<K> keys, Function<Collection<K>, Map<K, V>> func, int ttlSecs) {\n        // hit map\n        Map<K, V> hitMap = batchGet(keys);\n\n        // miss keys\n        Collection<K> missedKeys = null;\n        if (hitMap == null || hitMap.isEmpty()) {\n            missedKeys = keys;\n        } else {\n            missedKeys = keys.stream().filter(k -> !hitMap.containsKey(k)).collect(Collectors.toSet());\n        }\n\n        // check if no miss keys\n        if (missedKeys == null || missedKeys.isEmpty()) {\n            return hitMap;\n        }\n\n        // fetch miss key\n        Map<K, V> missMap = func.apply(missedKeys);\n        missMap.entrySet().forEach(e -> put(e.getKey(), e.getValue(), ttlSecs));\n\n        if (missMap == null || missMap.isEmpty()) {\n            // no miss map again\n            return hitMap;\n        } else {\n            // union & return\n            hitMap.putAll(missMap);\n            return hitMap;\n        }\n    }\n\n    default Map<K, V> batchCacheGet(Collection<K> keys, Function<Collection<K>, Map<K, V>> func) {\n        return batchCacheGet(keys, func, 0);\n    }\n\n    void put(K key, V value);\n\n    void put(K key, V value, int ttlSecs);\n\n    void del(K key);\n\n    default void batchDel(Collection<K> keys) {\n        if (keys != null) {\n            keys.stream().forEach(key -> del(key));\n        }\n    }\n\n    void clear();\n\n}\n```\n\n如上所示，我们定义了3大类Cache的基本操作:\n* get/batchGet: 从缓存中获取数据，支持单个或批量操作。\n* put/del: 向缓存中写入或删除，支持设置超时时间。\n* cacheGet: 若缓存中存在则直接返回，否则通过一个Function来生成结果，并写入缓存。类似的，支持单个和批量操作、支持设置超时时间。\n\n## LocalCache的实现\n\n在设计Memcached缓存之前，先来看一下本地缓存实现：\n```java\npublic class LocalCache<K, V> implements ICache<K, V> {\n\n    private Logger LOG = LoggerFactory.getLogger(getClass());\n\n    private Cache<K, V> gCache;\n\n    private long ttlSecs = 0;\n\n    public LocalCache(long capacity, long ttlSecs) {\n\n        CacheBuilder builder = CacheBuilder.newBuilder();\n        if (capacity > 0) {\n            builder.maximumSize(capacity);\n        }\n        if (ttlSecs > 0) {\n            this.ttlSecs = ttlSecs;\n            builder.expireAfterWrite(ttlSecs, TimeUnit.SECONDS);\n        }\n\n        this.gCache = builder.build();\n    }\n\n    @Nullable\n    @Override\n    public V get(K key) {\n        return gCache.getIfPresent(key);\n    }\n\n    @Override\n    public Map<K, V> batchGet(Collection<K> keys) {\n        if (keys == null || keys.isEmpty()) {\n            return new HashMap<>();\n        } else {\n            Map<K, V> result = new HashMap<>();\n            for (K key : keys) {\n                V val = gCache.getIfPresent(key);\n                if (val != null) {\n                    result.put(key, val);\n                }\n            }\n            return result;\n        }\n    }\n\n    @Override\n    public void put(K key, V value) {\n        gCache.put(key, value);\n    }\n\n    @Override\n    public void put(K key, V value, int curTtlSecs) {\n        if (curTtlSecs != this.ttlSecs) {\n            LOG.error(\"not support per-put ttlSecs currently\");\n        }\n        put(key, value);\n    }\n\n    @Override\n    public void del(K key) {\n        gCache.invalidate(key);\n    }\n\n    @Override\n    public void batchDel(Collection<K> keys) {\n        gCache.invalidateAll(keys);\n    }\n\n    @Override\n    public void clear() {\n        gCache.invalidateAll();\n    }\n}\n```\n\n如上所示，我们调用了Grava的缓存，来实现了本地缓存。\n\n要说明的是，由于Grava的设计限制，目前TTL需要在创建缓存之初就设定好，并不支持per-key的ttl设定。\n\n## MemcachedCache的实现\n\n在Spring Boot中集成Memcached，首先要选择一款基于Java的客户端，比较成熟的开源项目有:\n* spymemcached\n* XMemcached\n在本小节中，我们选择社区更为活跃的XMemcached，它支持线程池、一致性哈系等较为重要的特性。\n\n首先来看一下客户端的构造：\n```java\npublic class MemcachedClientBuilder2 {\n\n    public static MemcachedClient build(String serverList, int connPoolSize) throws IOException {\n        MemcachedClientBuilder builder = new XMemcachedClientBuilder(\n                AddrUtil.getAddresses(serverList));\n        // conn pool\n        builder.setConnectionPoolSize(connPoolSize);\n        // consistent hash\n        builder.setSessionLocator(new KetamaMemcachedSessionLocator());\n        return builder.build();\n    }\n\n}\n```\n\n如上所示，Builder主要设定了两个参数:\n* serverList: 服务器列表（形如ip:port，若有多个可通过空格分割开）\n* connPoolSize: 线程池大小\n\n\n如果你仔细观察ICache接口，可以发现它是泛型的ICache<K, V>，K和V可以是任意类型。\n\n然而，Memcached的设计较为轻量，Key必须是字符串，而Value则是byte数组。\n\n所以，需要设计一种通用的方式，以方便泛型数据类型到Memcached的Key/Value转换。\n\n我们将这一转换逻辑抽提成Key/Value的Transformer, Key的:\n```java\npublic interface CacheKeyTransformer<T> {\n\n    String getKey(T t);\n\n}\n\n```\n\n和Value的\n```java\npublic interface CacheValueTransformer<T> {\n\n    byte[] serialize(T obj);\n\n    T deserialize(byte[] bytes);\n\n}\n```\n\n这两个接口看起来很抽象，我们首先来看一下DefaultCacheKeyTransformer，实现了任何类型到String(Memcached Key类型)的转换:\n\n```java\npublic class DefaultCacheKeyTransformer<T> implements CacheKeyTransformer<T> {\n\n    private String cacheType;\n\n    public DefaultCacheKeyTransformer(String cacheType) {\n        this.cacheType = cacheType;\n    }\n\n    @Override\n    public String getKey(T t) {\n        return cacheType + \"#\" + t.toString();\n    }\n\n}\n```\n\n一种很常见的场景，是将对象序列化为Json然后放到Memcached的Value中，JsonCacheValueTransformer完成了这一过程:\n```java\npublic class JsonCacheValueTransformer<T> implements CacheValueTransformer<T> {\n\n    protected final Logger LOG = LoggerFactory.getLogger(getClass());\n\n    private ObjectMapper objectMapper;\n\n    private Class<T> cls;\n\n    public JsonCacheValueTransformer(Class<T> cls) {\n        this.objectMapper = new ObjectMapper();\n        this.cls = cls;\n    }\n\n    @Override\n    public byte[] serialize(T o) {\n        byte[] defReturn = new byte[1];\n        try {\n            if (o == null) {\n                return defReturn;\n            }\n            return objectMapper.writeValueAsBytes(o);\n        } catch (Exception e) {\n            LOG.error(\"JsonCacheValueTransformer serialize exception\", e);\n            return defReturn;\n        }\n    }\n\n    @Override\n    public T deserialize(byte[] bytes) {\n        try {\n            if (bytes == null) {\n                return null;\n            }\n            return objectMapper.readValue(bytes, cls);\n        } catch (Exception e) {\n            LOG.error(\"JsonCacheValueTransformer deserialize exception\", e);\n            return null;\n        }\n    }\n}\n```\n\n如上所示，我们应用了Jackson来实现了Json的序列化(反序列化)，并适配了byte数组到字符串的转换。\n\n此外，Cache的Value中直接存储Integer/Long/String也较为常见，感兴趣的可以直接查看[lmsia-cache项目的源代码](https://github.com/liheyuan/lmsia-cache)，这里不再赘述。\n\n实现了Key/Value的序列化之后，我们看一下具体的MemcachedCache实现:\n```java\npublic abstract class AbstractMemcachedCache<K, V> implements ICache<K, V> {\n\n    protected final Logger LOG = LoggerFactory.getLogger(getClass());\n\n    private static final int connPoolSize = 16;\n\n    protected abstract MemcachedClient getMemcachedClient();\n\n    protected abstract CacheKeyTransformer<K> getKeyTransformer();\n\n    protected abstract CacheValueTransformer<V> getValueTransformer();\n\n    private Transcoder<byte[]> transcoder = new Transcoder<byte[]>() {\n\n        @Override\n        public void setPrimitiveAsString(boolean primitiveAsString) {\n        }\n\n        @Override\n        public void setPackZeros(boolean packZeros) {\n        }\n\n        @Override\n        public void setCompressionThreshold(int to) {\n        }\n\n        @Override\n        public void setCompressionMode(CompressionMode compressMode) {\n        }\n\n        @Override\n        public boolean isPrimitiveAsString() {\n            return false;\n        }\n\n        @Override\n        public boolean isPackZeros() {\n            return false;\n        }\n\n        @Override\n        public CachedData encode(byte[] o) {\n            return new CachedData(0, o);\n        }\n\n        @Override\n        public byte[] decode(CachedData d) {\n            if (d != null) {\n                return d.getData();\n            } else {\n                return null;\n            }\n        }\n\n    };\n\n    public void init() throws Exception {\n\n        // check\n        if (getKeyTransformer() == null) {\n            throw new RuntimeException(\"keyTransformer can not be null\");\n        }\n\n        if (getValueTransformer() == null) {\n            throw new RuntimeException(\"valueTransformer can not be null\");\n        }\n\n    }\n\n\n    @Nullable\n    @Override\n    public V get(K key) {\n        try {\n            byte[] bytes = getMemcachedClient().get(getKeyTransformer().getKey(key), transcoder);\n            if (bytes == null) {\n                return null;\n            }\n            return getValueTransformer().deserialize(bytes);\n        } catch (Exception e) {\n            LOG.error(\"memcached get exception\", e);\n            return null;\n        }\n    }\n\n    @Override\n    public Map<K, V> batchGet(Collection<K> keys) {\n        if (keys == null || keys.isEmpty()) {\n            return new HashMap<>();\n        }\n\n        Map<K, String> key2idMap = new HashMap<>();\n        for (K key : keys) {\n            key2idMap.put(key, getKeyTransformer().getKey(key));\n        }\n\n        Collection<String> ids = key2idMap.values();\n\n        try {\n            Map<String, byte[]> map = getMemcachedClient().get(ids, transcoder);\n            if (map == null || map.isEmpty()) {\n                return new HashMap<>();\n            }\n\n            Map<K, V> result = new HashMap<>();\n            for (Entry<K, String> entry : key2idMap.entrySet()) {\n                K key = entry.getKey();\n                String id = entry.getValue();\n\n                byte[] bytes = map.get(id);\n                if (bytes != null) {\n                    result.put(key, getValueTransformer().deserialize(bytes));\n                }\n            }\n            return result;\n\n        } catch (Exception e) {\n            LOG.error(\"batchGet exception\", e);\n            return new HashMap<>();\n        }\n    }\n\n    @Override\n    public void put(K key, V value) {\n        put(key, value, 0);\n    }\n\n    @Override\n    public void put(K key, V value, int ttlSecs) {\n        try {\n            getMemcachedClient().add(\n                    getKeyTransformer().getKey(key),\n                    ttlSecs,\n                    getValueTransformer().serialize(value));\n        } catch (Exception e) {\n            LOG.error(\"memcached put exception\", e);\n        }\n    }\n\n    @Override\n    public void del(K key) {\n        try {\n            getMemcachedClient().delete(getKeyTransformer().getKey(key));\n        } catch (Exception e) {\n            LOG.error(\"memcached del exception\", e);\n        }\n    }\n\n    @Override\n    public void clear() {\n        try {\n            getMemcachedClient().flushAll();\n        } catch (Exception e) {\n            LOG.error(\"memcached flushAll exception\", e);\n        }\n    }\n}\n```\n\n如上所示，AbstractMemcachedCache预留了3个抽象getter方法:\n* memcachedClient\n* keyTransfomer\n* valueTransfomer\n\n实现者可以根据自己的需求来实现。\n\n## 自动配置\n\n前面已经提到，MemcachedCache依赖MemcachedClient的实例。\n\n如果每次都要手动构造MemcachedClient，实在是有些繁琐，我们可以通过Spring Boot的自动配置来自动注入：\n```java\n@Configuration\n@ConfigurationProperties(prefix = \"memcached\")\npublic class MemcachedClientAutoConfiguration {\n\n    // Server list seperate by space\n    private String serverList;\n\n    // Connection Pool Size, default 64\n    private int connPoolSize = 64;\n\n    public String getServerList() {\n        return serverList;\n    }\n\n    public void setServerList(String serverList) {\n        this.serverList = serverList;\n    }\n\n    public int getConnPoolSize() {\n        return connPoolSize;\n    }\n\n    public void setConnPoolSize(int connPoolSize) {\n        this.connPoolSize = connPoolSize;\n    }\n\n    @Bean\n    @ConditionalOnMissingBean(MemcachedClient.class)\n    public MemcachedClient createMemcachedClient() throws IOException {\n        return MemcachedClientBuilder2.build(serverList, connPoolSize);\n    }\n\n\n}\n```\n\n如上所示，上述自动配置会扫描配置文件：\n* 若发现\"memcached\"开头的配置，会尝试解析其serverList和connPoolSize字段。\n* 若解析成功，会调用之前介绍的Builder，自动生成一个MemcachedClient。\n\n## Memcached的应用案例\n\n我们通过一个简单的案例来说明MemcachedCache的使用。\n\n设计一个接口，返回10秒内每个用户的第一次访问的时间戳。\n\n我们通过MemcachedCache来实现，首先定义Cache:\n```java\n@Service\npublic class TimestampMemcachedCache extends AbstractMemcachedCache<Integer, Long> {\n\n    @Autowired\n    private MemcachedClient client;\n\n    private CacheKeyTransformer<Integer> keyTransformer = new DefaultCacheKeyTransformer<>(\"timestamp\");\n\n    private CacheValueTransformer<Long> valueTransformer = new LongValueTransformer();\n\n    @Override\n    protected MemcachedClient getMemcachedClient() {\n        return client;\n    }\n\n    @Override\n    protected CacheKeyTransformer<Integer> getKeyTransformer() {\n        return keyTransformer;\n    }\n\n    @Override\n    protected CacheValueTransformer<Long> getValueTransformer() {\n        return valueTransformer;\n    }\n}\n```\n\n如上所示，我们定义了<Integer, Long>类型的Cache，其中Integer的Key表示用户Id，Long类型的Value表示时间戳。\n\n上述的MemcachedClient是自动注入的，我们需要做一下配置：\n```yaml\nmemcached.serverList: \"127.0.0.1:11211\"\n```\n\n在使用的Service中，如下使用:\n```yaml\n\n    @Autowired\n    private TimestampMemcachedCache cache;\n\n    @Override\n    public String getCacheTimestampByUserId(int usrId) {\n        return String.valueOf(cache.cacheGet(userId, key -> System.currentTimeMillis(), 10));\n    }\n\n```\n\n如上所示，我们用了cacheGet来分别缓存最新时间戳，过期时间设为10秒钟。\n\n通过这个例子，你一定体会到了：有了ICache等封装后，Memcached的使用变得非常简单。\n\n## 小结\n\n在本节中，我们设计了通过了ICache接口，并实现了LocalCache、MemcachedCache两种不同的Cache。其中，我们重点探讨了MemcachedClient实现的细节，包括MemcachedClient的自动注入、Memcached数据类型的转换(Transfomer)。\n\n## 拓展阅读\n\n1. 实际的应用中，缓存的更新是一个较为复杂的任务，建议阅读[缓存更新的套路](https://coolshell.cn/articles/17416.html)。\n"
  },
  {
    "path": "legacy/ms-storage/sb-mysql.md",
    "content": "# Spring Boot整合MySQL\n\n经过上一节的讨论，相信你已经有了一套可运维的MySQL服务器了，接下来的两节，我们来讨论如何在Spring Boot中整合MySQL。\n\n在Spring Boot中整合MySQL有很多方式，常见的有:\n* Spring Jdbc Template直接集成\n* Spring Data JPA集成\n* Hibernate集成\n* MyBatis集成\n\n使用过Spring框架开发的同学，可能对后两种比较熟悉。但是这两种方法过于重量级，本书将专注于前两种，其中：\n* Jdbc Template需要直接编写SQL语句，更加接近数据库底层，开发效率低、性能高。\n* Spring Data JPA可以自动生成部分参数、解析结果的语句，开发效率高，性能低一些。\n上述两种方法各有优略，大家可以根据实际情况作出选择。\n\n## 数据源配置\n\n无论是选用哪种集成方式，数据源的集成都是必不可少的。\n\n为了提升性能，一般会使用数据库连接池，我们采用Spring Boot默认的tomcat连接池，只需要如下依赖配置即可生效：\n```\n    compile 'org.springframework.boot:spring-boot-starter-jdbc'\n    compile 'mysql:mysql-connector-java:5.1.9'\n```\n\n接下来我们看一下数据源的配置，在application.yaml中添加:\n```yaml\nspring.datasource:\n  url: jdbc:mysql://mysql/lmsia_abc?rewriteBatchedStatements=true\n  username: lmsia\n  password: pass\n  testOnBorrow: true\n  validationQuery: SELECT 1\n  tomcat:\n    max-active: 500\n```\n\n如上所示，除了基本的url和用户名、密码外，还设定了一系列额外参数，这些都是生产环境建议设置的，解释一下：\n* testOnBorrow / validationQuery: 从连接池取出连接后，先检查是否可用。这主要是解决长时间空闲情况下MySQL Server的[Gone Away问题(]https://dev.mysql.com/doc/refman/8.0/en/gone-away.html)\n* tomcat.max-active: 连接池最大连接数设定为500，默认的100在高并发场景下可能不够。\n* rewriteBatchedStatements: 只有设置为true，才会默认启用batch模式，可提升批量写入的性能。\n\n添加了上述配置后，Spring Boot会自动生成DataSource以及NamedParameterJdbcTemplate。我们可以通过后者直接操作数据库。\n\n## 通过JDBCTemplate操作数据库\n\n在操作数据库前，我们先来看一下数据表结构：\n```sql\nCREATE TABLE IF NOT EXISTS `user` (\n    `id` BIGINT(20) NOT NULL AUTO_INCREMENT,\n    `name` VARCHAR(256) NOT NULL,\n    `createdTime` BIGINT(20) NOT NULL,\n    `updatedTime` BIGINT(20) NOT NULL,\n    PRIMARY KEY (`id`)\n);\n```\n\n在微服务开发中，一般会将表的一行映射成一个实体:\n```java\nimport lombok.Data;\n\n@Data\npublic class User {\n\n    private int id;\n\n    private String name;\n\n    private long createdTime;\n\n    private long updatedTime;\n\n}\n```\n\n其中上面的@Data是lombok的注解，用于帮我们自动生成getter和setter，感兴趣的同学可以看这lombok官方文档](https://projectlombok.org/)，这里不再详述。\n\n来看一下数据库操作，我们将其封装在了Repository中:\n```java\n@Repository\npublic class UserRepositoryImpl implements UserRepository {\n\n    protected Logger LOG = LoggerFactory.getLogger(getClass());\n\n    @Autowired\n    protected NamedParameterJdbcTemplate db;\n\n    private RowMapper<User> ROW_MAPPER = new BeanPropertyRowMapper(User.class);\n\n    @Override\n    public void add(User user) {\n        String sql = \"INSERT INTO `user`(`name`, `createdTime`, `updatedTime`) VALUES \" +\n                \"(:name, :createdTime, :updatedTime)\";\n        SqlParameterSource param = new BeanPropertySqlParameterSource(user);\n        KeyHolder keyHolder = new GeneratedKeyHolder();\n        db.update(sql, param, keyHolder);\n        LOG.info(\"insert succ, id = {}\", keyHolder.getKey().longValue());\n    }\n\n    @Override\n    public Optional<User> getUserById(int id) {\n        String sql = \"SELECT * FROM `user` WHERE `id` = :id\";\n        SqlParameterSource param = new MapSqlParameterSource(\"id\", id);\n        try {\n            return Optional.ofNullable(db.queryForObject(sql, param, ROW_MAPPER));\n        } catch (EmptyResultDataAccessException e) {\n            return Optional.empty();\n        }\n    }\n}\n```\n\n解读一下上面的代码:\n* 通过Autowired自动注入NamedParameterJdbcTemplate\n* JdbcTemplate上执行update和query来完成插入或查询\n* 查询参数通过SqlParameterSource传入，返回值的对象映射通过RowMapper完成。\n\n## 两个数据源\n\n在上面的数据源配置、数据库操作中，都存在一个假设：只有一个数据源。\n\n如果一个微服务要同时依赖多个数据库，需要做如下事情:\n* 配置不同的数据源，建议不要采用默认的spring.datasource前缀，这主要是为了避免@Autowired时命名冲突。\n* 为多个数据源手动声明Configuration，包含多组DataSource和JdbcTemplate\n\n例如我们现在要添加2个数据库的数据源，那么配置文件要变成:\n```yaml\n\ndb1.datasource:\n  url: jdbc:mysql://mysql/db1?rewriteBatchedStatements=true\n  username: db1\n  password: pass\n  testOnBorrow: true\n  validationQuery: SELECT 1\n  tomcat:\n    max-active: 500\n\ndb2.datasource:\n  url: jdbc:mysql://mysql/db2?rewriteBatchedStatements=true```\n  username: db2\n  password: pass\n  testOnBorrow: true\n  validationQuery: SELECT 1\n  tomcat:\n    max-active: 500\n```\n\n由于不采用默认的spring.datasource前缀了，Spring Boot默认不会激活自动配置，需要手动编写：\n```java\n@Configuration\n@EnableTransactionManagement\npublic class DataSourceConfiguration {\n\n    @Bean(name = \"db1JdbcTemplate\")\n    @Primary\n    public NamedParameterJdbcTemplate initDb1JdbcTemplate(\n            @Autowired @Qualifier(\"db1DataSource\") DataSource dataSource) {\n        return new NamedParameterJdbcTemplate(dataSource);\n    }\n\n    @Bean\n    @Primary\n    @ConfigurationProperties(prefix = \"db1.datasource\")\n    public DataSource db1DataSource() {\n        return DataSourceBuilder.create().build();\n    }\n\n\n    @Bean(name = \"db2JdbcTemplate\")\n    public NamedParameterJdbcTemplate initDb2JdbcTemplate(\n            @Autowired @Qualifier(\"db2DataSource\") DataSource dataSource) {\n        return new NamedParameterJdbcTemplate(dataSource);\n    }\n\n    @Bean\n    @ConfigurationProperties(prefix = \"db2.datasource\")\n    public DataSource db2DataSource() {\n        return DataSourceBuilder.create().build();\n    }\n\n    @Bean\n    public PlatformTransactionManager txManager() {\n        return new DataSourceTransactionManager(tutorClockinWriterDataSource());\n    }\n}\n```\n\n简单说明一下：\n* 根据自定义的前缀生成对应DataSource。\n* 根据DataSource生成对应的NamedParameterJdbcTemplate。\n* 因为要生成两组Datasource和NamedParameterJdbcTemplate，所以有一组要设置为@Primary，这是Spring Boot的要求。\n\n在使用时，因为有两个NamedParameterJdbcTemplate了，所以要补充一下名字以做区分，如下：\n```java\n@Autowired\n@Qualifier(\"db1\")\nprotected NamedParameterJdbcTemplate db1;\n\n@Autowired\n@Qualifier(\"db2\")\nprotected NamedParameterJdbcTemplate db2;\n```\n\n区分了不同的NamedParameterJdbcTemplate后，其余的数据库操作和一个Datasource时是完全相同的，这里不再赘述。\n\n# 通过JPA操纵数据库\n\n前面提到了，除了JdbcTemplate外，还可以使用JPA来操作数据库。\n\n由于spring-boot-starer-data-jpa显示以依赖了spring-boot-starter-jdbc，所以我们可以直接替换依赖:\n```grovvy\n    compile 'org.springframework.boot:spring-boot-starter-data-jpa'\n```\n\n这一步替换，将不会影响DataSource、JdbcTemplate的自动注入，JPA也是需要Datasource和JdbcTemplate才能正常完成工作的。\n\nSpring Boot JPA的默认实现是通过Hibernete完成的（JPA只是一套接口，Hibernete是接口的一种实现）。\n\njpa需要在yaml中添加一些特殊配置:\n```yaml\nspring.jpa.properties.hibernate.dialect: org.hibernate.dialect.MySQL5InnoDBDialect\nspring.jpa.hibernate.ddl-auto: validate\nspring.jpa.hibernate.naming.physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl\n```\n\n其中:\n* hibernate.dialect让Hibernete可以更高效的生成sql\n* ddl-auto设置为validate，不自动创建表但是会验证表与实体是否符合\n* naming.physical-strategy表字段名映射为驼峰命名\n\n针对要操作的实体，需要做一些特殊注解，以让JPA可以关联到对应的表上，为了对比说明，我们单独创建了一个对象UserForJpa:\n```java\nimport lombok.Data;\n\nimport javax.persistence.Column;\nimport javax.persistence.Entity;\nimport javax.persistence.GeneratedValue;\nimport javax.persistence.GenerationType;\nimport javax.persistence.Id;\nimport javax.persistence.Table;\nimport javax.persistence.Temporal;\nimport javax.persistence.TemporalType;\n\n/**\n * @author coder4\n */\n@Data\n@Entity\n@Table(name = \"user\")\npublic class UserForJpa {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private long id;\n\n    @Column(nullable = false)\n    private String name;\n\n    @Column(name = \"createdTime\", nullable = false, updatable = false)\n    private long createdTime;\n\n    @Column(name = \"updatedTime\", nullable = false)\n    private long updatedTime;\n\n}\n```\n\n说明一下：\n* 实体通过@Entity标注\n* @Table关联实体和表\n* @Id和@GeneratedValue完成自增主键的声明\n* @Column是普通列的声明，可设置是否nullable以及是否可更新\n\n在数据库操作中，我们可以完全让JPA帮我们生成sql，如下:\n```java\n@Repository\npublic interface UserJpaRepository extends JpaRepository<UserForJpa, Integer> {\n\n\n}\n```\n\n是的，你没有看错，我们不需要编写任何方法，就能自动获得save(), findOne(), findAll(), count(), delete()等接口，具体可以参见JpaRepository的源代码。\n\n我们看一下调用方式:\n```java\nuserJpaRepository.findOne(userId);\n\nuserJpaRepository.save(user);\n```\n\nJpaRepository提供的都是较为基础的操作，有事无法完全满足我们的需求。我们可以自行定义sql，如下：\n```java\n    @Query(\n            value = \"SELECT * FROM `user` ORDER BY `id` DESC LIMIT 1\",\n            nativeQuery = true)\n    UserForJpa findLatestUser();\n```\n\n如上所示，我们通过@Query注解实现了通过指定SQL查找最新注册的用户。\n\n## 小结\n\n在本小节中，我们首先介绍了Sping Boot中MySQL数据源的配置，随后，介绍了如何配置多个数据源并手动注入DataSource、JdbcTemplate。\n\n接下来，我们介绍了两种数据库操作方法:\n* JdbcTemplate更接近数据库底层，需要编写较多代码，性能较好\n* Spring JPA Data可以自动生成部分代码，开发效率高，性能稍差，且对POJO具有一定的侵入性\n\n上述两种方法各有优劣，大家可以根据实际需求进行选择。\n\n## 拓展阅读\n\n1. Tomcat数据库连接池的详细配置参数可以参考[官方参数文档](https://tomcat.apache.org/tomcat-8.0-doc/jdbc-pool.html#Common_Attributes)\n1. Spring JPA Data更详细的用法可以参考[Spring JPA Data官方文档](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/)\n1. Spring JDBC更详细的用法可以参考[Spring JDBC官方文档](https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html)\n\n"
  },
  {
    "path": "legacy/ms-storage/sb-redis.md",
    "content": "# Spring Boot整合Redis\n\n在上一章中，我们讨论了Redis服务的运维，包括单机运行和Sentinel运行。\n\n在本小节中，我们讨论如何在Spring Boot中集成Redis。\n\nSpring Boot内置了Redis的接入方式，spring-data-redis，这种方案在Jedis客户端的基础上尽心过了简单的封装。若只使用Redis的KV存储特性，该方案可以满足要求。但对于Redis的高级特性(如SortedSet、SETNX等)，则需要手动调用底层Jedis客户端的API，使用方式较为晦涩且容易出错。\n\n为此，我们推荐使用Redisson作为接入客户端，它提供了简单易用的封装，可以用最小的编程代价来发挥Redis的最大功能。\n\n## 库依赖及自动配置\n\n为了方便类库的复用，我们将Redisson的依赖及自动配置抽成一个单独的项目[lmsia-redis ](https://github.com/liheyuan/lmsia-redis)。\n\n首先来看一下依赖：\n```groovy\n    compileOnly 'org.springframework.boot:spring-boot-autoconfigure:1.5.6.RELEASE'\n    compileOnly 'com.fasterxml.jackson.core:jackson-databind:2.9.0'\n    compileOnly 'ch.qos.logback:logback-classic:1.2.3'\n\n    compile 'org.redisson:redisson:3.7.3'\n\n    // Use JUnit test framework\n    testCompile 'junit:junit:4.12'\n```\n\n如上述的代码片段所示，编译依赖了Sping Boot的自动注解、jackson、以及logback，此外显式依赖了redisson库。\n\n上一小节已经介绍，Redis有单机、Sentinel、Cluster三种部署方式，我们这里介绍前两种。\n\n首先看一下单机Redis的自动配置：\n```java\npackage com.coder4.sbmvt.redis.configuration;\n\nimport com.coder4.sbmvt.redis.utils.RedissonUtils;\nimport org.redisson.Redisson;\nimport org.redisson.api.RedissonClient;\nimport org.redisson.config.Config;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\nimport java.io.IOException;\n\n/**\n * @author coder4\n */\n@Configuration\n@ConfigurationProperties(prefix = \"redis\")\npublic class RedissonAutoConfiguration {\n\n    // server list\n    private String server;\n\n    // redis password\n    private String password;\n\n    // connection pool size, default 128\n    private int connPoolSize = 128;\n\n    // retry interval in ms\n    private int retryInterval = 100;\n\n    @Bean(destroyMethod = \"shutdown\")\n    @ConditionalOnMissingBean(RedissonClient.class)\n    public RedissonClient createRedissonClient() throws IOException {\n        if (getServer() == null || getServer().isEmpty()) {\n            throw new IllegalArgumentException(\"server is empty\");\n        }\n\n        Config config = new Config();\n\n        config.useSingleServer()\n                .setAddress(RedissonUtils.wrapSchema(server))\n                .setPassword(password)\n                .setRetryInterval(retryInterval)\n                .setConnectionPoolSize(connPoolSize);\n\n        return Redisson.create(config);\n    }\n\n\n    public String getServer() {\n        return server;\n    }\n\n    public void setServer(String server) {\n        this.server = server;\n    }\n\n    public String getPassword() {\n        return password;\n    }\n\n    public void setPassword(String password) {\n        this.password = password;\n    }\n\n    public int getConnPoolSize() {\n        return connPoolSize;\n    }\n\n    public void setConnPoolSize(int connPoolSize) {\n        this.connPoolSize = connPoolSize;\n    }\n\n    public int getRetryInterval() {\n        return retryInterval;\n    }\n\n    public void setRetryInterval(int retryInterval) {\n        this.retryInterval = retryInterval;\n    }\n}\n```\n\n如上所示:\n* 若yaml配置中包含\"redis\"前缀的配置，则注解被激活。\n* 尝试解析server、password、connPoolSize、retryInterval4个配置字段。\n * server是redis服务器的IP:Port\n * password是redis服务器的密码\n * connPoolSize是连接池默认大小，默认是128\n * retryInterval是命令执行失败后的重试间隔，默认是100ms\n* 根据上述配置自动生成ResissonClient\n\n再来看一下Sentinel方式的自动配置：\n```java\npackage com.coder4.sbmvt.redis.configuration;\n\nimport com.coder4.sbmvt.redis.utils.RedissonUtils;\nimport org.redisson.Redisson;\nimport org.redisson.api.RedissonClient;\nimport org.redisson.config.Config;\nimport org.redisson.config.ReadMode;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\nimport java.io.IOException;\nimport java.util.List;\n\n@Configuration\n@ConfigurationProperties(prefix = \"redis-sentinel\")\npublic class RedissonSentinelAutoConfiguration {\n\n    // server list\n    private String sentinelServerList;\n\n    // sentinel master name\n    private String masterName;\n\n    // redis password\n    private String password;\n\n    // connection pool size, default 128\n    private int connPoolSize = 128;\n\n    // retry interval in ms\n    private int retryInterval = 100;\n\n    @Bean(destroyMethod = \"shutdown\")\n    @ConditionalOnMissingBean(RedissonClient.class)\n    public RedissonClient createRedissonClient() throws IOException {\n        List<String> sentinelAddrs = RedissonUtils.splitStr(sentinelServerList);\n        if (sentinelAddrs == null || sentinelAddrs.size() == 0) {\n            throw new IllegalArgumentException(\"sentinel address is empty\");\n        }\n\n        Config config = new Config();\n\n        config.useSentinelServers()\n                .setMasterName(masterName)\n                .addSentinelAddress(sentinelAddrs.stream().map(RedissonUtils::wrapSchema).toArray(String[]::new))\n                .setPassword(password)\n                .setMasterConnectionPoolSize(connPoolSize)\n                .setSlaveConnectionPoolSize(connPoolSize)\n                .setRetryInterval(retryInterval)\n                .setReadMode(ReadMode.MASTER);\n\n        return Redisson.create(config);\n    }\n\n\n    public String getSentinelServerList() {\n        return sentinelServerList;\n    }\n\n    public void setSentinelServerList(String sentinelServerList) {\n        this.sentinelServerList = sentinelServerList;\n    }\n\n    public String getMasterName() {\n        return masterName;\n    }\n\n    public void setMasterName(String masterName) {\n        this.masterName = masterName;\n    }\n\n    public String getPassword() {\n        return password;\n    }\n\n    public void setPassword(String password) {\n        this.password = password;\n    }\n\n    public int getConnPoolSize() {\n        return connPoolSize;\n    }\n\n    public void setConnPoolSize(int connPoolSize) {\n        this.connPoolSize = connPoolSize;\n    }\n\n    public int getRetryInterval() {\n        return retryInterval;\n    }\n\n    public void setRetryInterval(int retryInterval) {\n        this.retryInterval = retryInterval;\n    }\n}\n```\n\n上述自动配置和RedissonAutoConfiguration基本一致，唯一的差别是配置了Sentinal服务集群列表和masterName。\n\n最后，在别的项目引用这个包时，我们要将上述两个自动配置暴露给Spring Boot扫描，添加到spring.factories中：\n```\norg.springframework.boot.autoconfigure.EnableAutoConfiguration=\\\ncom.coder4.sbmvt.redis.configuration.RedissonAutoConfiguration,\\\ncom.coder4.sbmvt.redis.configuration.RedissonSentinelAutoConfiguration\n```\n\n接下来，我们来看一下在Spring Boot中的具体集成方法。\n\n## Spring Boot中集成Redis\n\n在Spring Boot中集成Redis，首先依赖刚才的lmsia-redis类库:\n```groovy\ncompile 'com.github.liheyuan:lmsia-redis:0.0.4'\n```\n\n然后在yaml中添加配置：\n```yaml\n# redis\nredis.server: \"192.168.99.100:6379\"\n```\n\n经过上述配置后，Spring Boot在启动后，会自动注入RedissonClient，我们可以直接Autowired使用：\n\n```java\n@Service\npublic class MyListRedissonImpl implements MyListRepository {\n\n    @Autowired\n    private RedissonClient redissonClient;\n\n    private static String getKey(int userId) {\n        return String.format(\"list:%d\", userId);\n    }\n\n    private RSet<Long> obtainSet(int userId) {\n        return redissonClient.getSet(getKey(userId), new LongCodec());\n    }\n\n    @Override\n    public List<Long> get(int userId) {\n        return new ArrayList(obtainSet(userId).readAll());\n    }\n\n    @Override\n    public void add(int userId, long data) {\n        obtainSet(userId).add(data);\n    }\n\n```\n\n我们通过上述代码，简单看一下Redisson的用法。\n* 通过getKey拼接一个key\n* 通过redissonClient.getSet获取key对应的RSet，编码是Long，这里的RSet和Java的Set完全兼容。\n* add进行添加、get返回set中全部数据。\n\nRedisson将较为繁琐的Redis命令进行了封装和组合，我们操作的是类Java的数据结构，但实际底层命令都是Redis的。\n\n关于Sentinel的配置方法，是类似的，这里不再赘述。\n\n至此，我们完成了Spring Boot中Redis服务的整合工作。\n\n# 拓展阅读\n\n1. Redisson还提供了锁、排序集合等许多高级数据结构，可以参考[Redisson官方文档](https://github.com/redisson/redisson/wiki/Table-of-Content)\n"
  },
  {
    "path": "legacy/spring-boot/README.md",
    "content": "# 微服务的开发框架 \n\n在[上一章](../ms-discovery/README.md)，我们解决了微服务的第一个核心问题\"服务发现\"。\n\n本章我们将回归开发本质，开始上手微服务的开发框架。\n\n本书选用Java作为微服务的开发语言，选用Spring Boot作为开发框架。作为经典J2EE框架Spring的\"继承者\"，Spring Boot具有快速上手、轻松集成后端组件、高效配置、代码依赖注入等诸多特性，能够很好地适应微服务\"快速开发\"、\"快速迭代\"的理念。\n\n本章将从微服务代码的文件结构入手，首先看一下采用Gralde构建工具后，如何实现子项目的划分，以及这样划分的意义。接着我们会看一下各个子模块下，代码的层次结构。\n\n在熟悉了微服务的代码结构后，我们来具体看一下微服务的\"核心服务\"功能：RPC服务和REST服务，在分析代码的基础上，我们将探讨如何将它们与服务发现机制进行融合。\n\n章节的最后，我们将探索一些常用工具类，并介绍\"单元测试神器\"Mockito的使用方法。\n"
  },
  {
    "path": "legacy/spring-boot/discovery.md",
    "content": "# Spring Boot + k8s 服务发现\n\n"
  },
  {
    "path": "legacy/spring-boot/gerrit.md",
    "content": "# Spring Boot 多模块Gradle项目\n\n"
  },
  {
    "path": "legacy/spring-boot/graceful-shutdown.xml",
    "content": "<mxfile userAgent=\"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.170 Safari/537.36\" version=\"8.6.5\" editor=\"www.draw.io\" type=\"device\"><diagram id=\"a897b486-16f4-0dce-ec20-64fbc7c553e4\" name=\"Page-1\">7Vlbk5owFP41PraDBBEfV/fSTrednTrTy1MnwBHSBkJDWLW/vokkAgLd7Sxqu60vhnMJyfm+c3KiI7RINjccZ/FbFgId2Va4GaHLkW2PHduVX0qyLSVTTwsiTkJtVAmW5AdooaWlBQkhbxgKxqggWVMYsDSFQDRkmHO2bpqtGG2+NcMRtATLANO29CMJRVxKvYlVyV8BiWLz5rGlNT4OvkWcFal+38hGq92nVCfYzKXt8xiHbF0ToasRWnDGRDlKNgugKrYmbKXfdY92v24OqXiUg/bIxdbsHUIZCv3IuIhZxFJMryrpfLc/UDNY8ikWCZXDsRzChohPtfFnZfJyop5SwbeftMfuodJ9BSG2mgO4EEyKqvfeMpbpGdt7M4tnBQ/06pFmC+YRaKtpKVL7qrnpeNwAS0AuRxpwoFiQ+yYFsGZStLeroikHOqA9wbWff3C9swUXPf/gzs4VXL2We0wLMIXMpXJV8xWTW6hH3f1eMKN4ke9icSENxm62qZRyFKnvN4UPPAUha7sqTOWUPjfq93eLLx9e30ndNVNCnMgIzlM/z8o5DswPLZbA74kMp23tjeVGyxUbjwPKNAmxjomAZYZ3kKzl0dYkyYpQumBULk35ohCDtwqkPBecfYOaxg088Fe/Av4euIDNL0HVWjRzSpetOVz10bGuDqaxOU7i2qHkWU/ngT3s8XCqVHLaqWQidPpcco6USxXbLx7LdtltZGpYJPQiEIqtc0VEIhufW+wDvWM5EYSl0sRnQrBEGlClmO/bmhrLdWNTzXFBSaR8BTtIHFYISlKZIaZbs4bJDQ81U2PSTg3UkRnuAJkxPS6qHdXuPWRURll59uLdrpC99W78+/VuAl7odNU7z/aR6w6D6dQaN0CdPrLcDQGqd0ZQ7X8J1K5MPRqqsyOhambJM5wOxw25m3LCfviHWHxFvAd41yN+YkLEhbhk6/Q8nF95AQSdjZvvTZzJQIfTIedtZJ+O8/Zzu3qaQNV7OnSuls4s5tT3o8t3y/568PdfdcYzt3lKnPSqM8Cd1+nC9BXLRYoT6EfuMZKH0JUhFk0Im1ClLIUDXLUI6649kMBB/Upg2vmEhOGuPnRxpsmqIUhgyqQhgTtrkcDp4IA9BAcGuKt1ckD9pvEw1neyPP5ngI3QQbPoei9n9Y9zOkK4RyLELcNhZ4lvSyQ8OA3gn6PBBLl/Cg0MIQenwQhd/424tkDsgLoXV9f8JLPvi91jASkfq7/Sdrra/5Xo6ic=</diagram></mxfile>"
  },
  {
    "path": "legacy/spring-boot/mockito.md",
    "content": "# Mockito 单元测试打桩神器\n\n"
  },
  {
    "path": "legacy/spring-boot/rest-nginx.xml",
    "content": "<mxfile userAgent=\"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36\" version=\"8.6.6\" editor=\"www.draw.io\" type=\"device\"><diagram id=\"a897b486-16f4-0dce-ec20-64fbc7c553e4\" name=\"Page-1\">7Vtbl9o2EP41PCbHtmxjHlmym/R0m3JK2qRPObItjBrZcoVYoL++Mpbwlaw3+LJA/II9us83nzQjiRGYhbv3DMar36iPyMjQ/N0IvBsZ4hnb4ieR7FPJ2JKCgGE/FemZYIH/Q1KoSekG+2hdyMgpJRzHRaFHowh5vCCDjNFtMduSkmKrMQxQRbDwIKlKP2Ofr1KpY2mZ/APCwUq1rGsyxYXet4DRTSTbGxlgeXjS5BCqumT+9Qr6dJsTgfsRmDFKefoW7maIJLpVakvLPZxIPfaboYg3KiBLPEGykWNnsmd8r7SBfKEc+UkZX9GARpDcZ9K7w4hRUqcmvlY8JOJVF69oh/mX3PvfSZa3VvIVcbb/IkscPrK0fxDne2kVcMOpEGXtPlIayxqro5XDWdMN82TvgbQfyAIkc41TUTKuXDGpofeIhkh0R2RgiECOn4pGAaVtBcd8mX7Fi1TxCXUb169cZzDlgutX7mQo5YLKPDEybCJ6dbekYgh5rdv/bqhKeLM+6GIqMuh2vMsSxVuQ/P66cRGLEBezfTJVpVW6TCV/+PRp/vWvX+Yi8YEmUhgKFd5F7jpOKynlL+dYIPaEhT4N7ZhZjDTtsipRspmiRWxXmKNFDA+YbMVqV7SSJSZkRonoWlIW+BA5S0/I15zRbyiXYnsOcpffQ/4JMY5230VVpoKJmRaRq6tuytVkm61VulphVrl1ytFaMIRx9ywDpUcmzhHDor+IyXpeRrYSZ55ln1llX7pw9MA2syO2TSOfUdGIoc0IRgcSVAnUiCXCcYmT101Iph5PrPwuMWAsfKhH6CIyp2vMMY1EFpdyTkORgSQJd0cPKccO6SNldUwJDpKynJYIRzec4EgwSzl+WjucemMo506SCmh6hVSghlN2C5wadwS3nP1q5sk/UEyEnpOSJxGvmsbJmVJ/+UxpIcc362ZKx3CBbbeD6liBqOKQhhNlG6A6A4Jq3BKoutUjqpMBUQW3hKoBjP5QNa4tLlOKynsvoNYPOsObORSdMgb3uQwxxRFf52qeJ4Kc8zourbOyhYeG+XVHKyGb9iDD+TiURtBbFxc1vtSRrbMF07lE7E3LaBV7pZm+o9p3Hxc/7nm//vhUnxjFqdyputKdxadtxE5mHabJJsQpzDKJozk1gDfFViiYFwEsAhXRCJVQlSIoAyZPwIby0ZiKpELs+4e5q85iijbVggkA3S4yXa/6aHaNBRhtWIDdkQU8UujX8rkqEXjAKNl3ujHcLVDEXbedt5P8Y1bMwOzIDFRPrnhhH1cX9twpUrdbVJ1tWnwMcLQbZnlcLpeGV7s8+rZrW1mkUyFEDVant5rMolfT6/Ko2n4F27cDEsepIU5fe7udbQzh34VXeY0bu+exrbyxa6pAuIeNXeD8ZNtxI2cQtqnGW2fbZ+T+ZNvzbNOd/thmtnBIXev4/xkvOEMwbOL5X0fwd5YR6KpIFvwNFQSY1sUHAYUz6RrHxZw0xqf9c+rLj7GeU292528A9fZw12Jo9TafXdpXb1eeeG+XmqavatP4rEUDTEDRTVd3JXuJijs+1p1WQb2cGxhnwTrWSkc+ZkNY23AI1dw9CKzGLcFqGn3CWr203h+sr/0SRquwWlrVVe8O1hZC9Rs+tzkLeAtYRT6bfYVs4jP7u0t6hp/9pwjc/w8=</diagram></mxfile>"
  },
  {
    "path": "legacy/spring-boot/sb-gradle-structure.md",
    "content": "# Gradle子项目划分与微服务的代码结构\n\n## Gradle简介\n\n如前序章节[微服务技术栈概览](../architecture/microservics.md)所述，本书选用Java作为开发语言、Gradle作为构建工具。\n\n与Maven相比，Gradle具有如下优势：\n* 灵活性：Gradle内置了脚本支持，可以实现更强大、更灵活的构建功能。\n* 高性能：Gradle支持并行编译、多级缓存，最高可节省90%的编译时间[^1]。\n* 易于维护：与xml相比，Gradle的依赖描述语言更简洁，更易于维护。\n* 无缝兼容：Gradle无缝兼容Maven，已有的系统也可以轻松地迁移过来。\n\n## 微服务架构下Gradle的子项目划分\n\n在[微服务的自动发现与负载均衡](ms-discovery/README.md)一章中，我们已经构建了一个微服务项目\"lmsia-abc\"，让我们来看一下它的目录结构。为了清晰起见，只展示一层目录结构：\n\n```shell\n.\n├── build.gradle\n├── gradle\n│   └── wrapper\n├── gradlew\n├── gradlew.bat\n├── lmsia-abc-client\n│   ├── build\n│   ├── build.gradle\n│   ├── out\n│   └── src\n├── lmsia-abc-common\n│   ├── build\n│   ├── build.gradle\n│   ├── out\n│   └── src\n├── lmsia-abc.iml\n├── lmsia-abc-job\n│   ├── build\n│   ├── build.gradle\n│   ├── out\n│   └── src\n├── lmsia-abc-server\n│   ├── build\n│   ├── build.gradle\n│   ├── out\n│   └── src\n├── settings.gradle\n└── tool\n    ├── compileThrift.sh\n    └── shutdown.sh\n\n```\n\n我们来逐一进行讲解：\n* 主项目级别Gradle配置文件: build.gradle和settings.gradle，定义了子项目，以及子项目共用的依赖、仓库等，我们会在稍后展开讲解。\n* gradle最小化构建工具: gradle构建工具初始化后，会在项目中生成gradle、gradlew、gradlew.bat，这些是最小化的构建工具，方便项目移植后的构建。\n* lmsia-abc-common: 如前文所属，我们的项目采用Thrift RPC。我们将Thrift的dsl文件、自动生成的Java(客户端桩)代码放置在common子项目中。这样，如果有其他微服务需要依赖相关数据结构，只需要依赖'lmsia-abc-common'即可。\n* lmsia-abc-client: 在引用common包后，可以自行构造Thrift客户端，从而完成RPC调用。然而，这一过程较为繁琐。试想有一个提供用户信息的微服务，因为较为基础，有20个微服务依赖它，那么就需要20次书写重复的代码。\"重复代码乃万恶之源\"，为了解决Thrift客户端重复生成的问题，我们创建了client子项目，负责生成Thrift客户端，并添加自动配置（如果没有接触过Spring Boot，可能会不理解自动配置，没有关系，我们很快就会作出解释）。\n* lmsia-abc-server: 微服务的核心，即提供\"服务\"。我们将Thrift、RPC服务的逻辑代码封装在server子项目中。\n* lmsia-abc-job: 在微服务业务的升级、演进过程中，可能会需要对数据作出修正。这些代码可能只需要执行一次，因此不需要放入server子项目提供服务，我们将他们放入job子项目中。\n* tool: 一些提升微服务开发的效率工具，我们将在[开发效率脚本](../toolchain/spring-boot-scripts.md)一节中进行介绍。\n\n由于篇幅所限，我们不会对Thrift进行入门介绍，如果你无法理解上述Thrift的DSL、自动代码生成等内容，可以参考[官方教程](http://thrift.apache.org/tutorial/java)。\n\n我们来看一下根路径下的build.gradle\n```shell\nbuildscript {\n\n    ext {\n        springBootVersion = '1.5.6.RELEASE'\n    }\n\n    repositories {\n        maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }\n        maven { url 'https://jitpack.io' }\n    }\n \n    dependencies {\n        classpath(\"org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}\")\n    }\n\n}\n\nsubprojects {\n\n    apply plugin: 'java'\n    apply plugin: 'idea'\n    apply plugin: 'org.springframework.boot'\n    \n    sourceCompatibility = 1.8\n    targetCompatibility = 1.8\n\n    group = 'com.coder4.lmsia'\n    version = '0.0.1'\n\n    repositories {\n        maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }\n        maven { url 'https://jitpack.io' }\n        mavenLocal()\n    }\n\n}\n\nrepositories {\n    maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }\n    maven { url 'https://jitpack.io' }\n}\n```\n\n我们来顺序解释上述文件：\n* buildscript: 定义了gradle自身所需要使用的资源，包含Spring Boot插件和maven的仓库地址。\n* subprojects: 定义了子项目(common, client, job, server)所需要使用的共用部分，Java、IDEA、Spring Boot插件、Javac版本、项目的group, version，以及仓库，这里的仓库是给子项目使用的，看似与buildscript的定义重复，但确实是必要的。\n* repositories: 定义主项需要的仓库地址，与上面类似，这里也是必须的，并不是冗余定义。\n\n在settings.gradle中，定义了各个子项目的路径：\n```shell\ninclude 'lmsia-abc-common'\ninclude 'lmsia-abc-client'\ninclude 'lmsia-abc-job'\ninclude 'lmsia-abc-server'\n```\n\n下面，我们来看一下子项目中的gradle文件，以'lmsia-abc-server/build.gradle'为例：\n```shell\ndependencies {\n    compile project(':lmsia-abc-common')\n\n    compile 'org.springframework.boot:spring-boot-starter-web'\n\n    compile 'com.github.liheyuan:lmsia-thrift-server:0.0.1'\n    compile 'com.github.liheyuan:lmsia-commons-http:0.0.1'\n\n    testCompile 'org.springframework.boot:spring-boot-starter-test'\n}\n```\n\n由于我们将子项目共用的部分抽取到根目录的build.gradle中，所以上述子项目的gradle文件就十分简单了。\n\n上述文件表明：server子项目依赖common子项目，同时依赖了'spring-boot-starter-web'、'lmsia-thrift-server'、'lmsia-commons-http'两个项目，测试依赖'spring-boot-starter-test'。细心的读者可能已经发现，'spring-boot-starter-web'和'spring-boot-starter-test'并没有定义版本号。这就是我们在根文件中定义的'Spring Boot插件'所完成的工作之一。\n\n## common子项目的代码结构\n\n我们来看一下common子项目的结构：\n```shell\n├── build.gradle\n└── src\n    └── main\n        ├── java\n        │   └── com\n        │       └── coder4\n        │           └── lmsia\n        │               └── abc\n        │                   ├── constant\n        │                   │   └── LmsiaAbcConstant.java\n        │                   └── thrift\n        │                       └── LmsiaAbcThrift.java\n        └── thrift\n            └── lmsiaAbc.thrift\n\n```\n\n我们解释一下目录结构：\n* 除了build.gradle外，代码被放置在src/main/java下，这是gradle推荐的默认路径。\n* thrift的DSL文件放置在'src/main/thrift'下\n* 编译好的Thrift桩文件在'src/main/java`下\n\n## client子项目的代码结构\n\n接下来，我们看一下client子项目的目录结构：\n```shell\n├── build.gradle\n└── src\n    ├── main\n    │   ├── java\n    │   │   └── com\n    │   │       └── coder4\n    │   │           └── lmsia\n    │   │               └── abc\n    │   │                   └── client\n    │   │                       ├── configuration\n    │   │                       │   └── LmsiaAbcClientConfiguration.java\n    │   │                       ├── LmsiaAbcEasyClientBuilder.java\n    │   │                       └── LmsiaK8ServiceClientBuilder.java\n    │   └── resources\n    │       └── META-INF\n    │           └── spring.factories\n    └── test\n        └── java\n            └── com\n                └── coder4\n                    └── lmsia\n                        └── abc\n                            └── client\n                                ├── LmsiaAbcEasyClientTest.java\n                                └── LmsiaAbcK8ServiceClientTest.java\n\n```\n\n* 自动配置: 代码包的LmsiaAbcClientConfiguration和资源包的spring.factories，一起实现了自动配置。当别的项目通过maven引用这个client包时，配置会自动生效，生成可注入的客户端实例。\n* Builder: 方便手动或自动配置的调用，用于生成客户端实例。\n* 测试: 'src/test'里面内置了两个测试。\n\n## server子项目的代码结构\n\n看一下server子项目的目录结构：\n```shell\n.\n├── build.gradle\n└── src\n    ├── main\n    │   ├── java\n    │   │   └── com\n    │   │       └── coder4\n    │   │           └── lmsia\n    │   │               └── abc\n    │   │                   └── server\n    │   │                       ├── configuration\n    │   │                       │   └── ThriftProcessorConfiguration.java\n    │   │                       ├── LmsiaAbcApplication.java\n    │   │                       ├── rest\n    │   │                       │   ├── controller\n    │   │                       │   │   └── AbcController.java\n    │   │                       │   ├── logic\n    │   │                       │   │   ├── impl\n    │   │                       │   │   │   └── AbcLogicImpl.java\n    │   │                       │   │   └── intf\n    │   │                       │   │       └── AbcLogic.java\n    │   │                       │   └── wrapper\n    │   │                       ├── service\n    │   │                       │   ├── impl\n    │   │                       │   │   └── HelloServiceImpl.java\n    │   │                       │   └── intf\n    │   │                       │       └── HelloService.java\n    │   │                       └── thrift\n    │   │                           └── ThriftServerHandler.java\n    │   └── resources\n    │       ├── application.yaml\n    │       └── logback-spring.xml\n    └── test\n        └── java\n            └── com.coder4.lmsia.abc\n                └── server\n                    └── LmsiaAbcTest.java\n\n```\n\n解释一下文件：\n* RPC服务相关：\n * 自动配置: 'server.configuration.ThriftProcessorConfiguration'是RPC服务的自动配置，用于自动启动RPC服务，我们后面会对此详细讲解。\n * RPC入口函数: server.thrift.thrift.ThriftServerHandler定义了RPC的入口函数\n* REST服务：REST服务放在server.rest包下，并进行了进一步分层\n * Spring MVC: Controller在rest.controller下\n * REST逻辑: 为了防止Controller过于臃肿，我们将Controller的逻辑都放在了rest.logic中。该包又分为intf和impl，前者是Interface(接口)，后者是Implementation(实现)。\n * Wrapper: 如果Logic中需要对REST接口进行包装，可以放在wrapper里\n* 业务逻辑: 我们将所有业务逻辑抽象出来，放到server.service下，与Logic类似，也分为intf和impl\n* 配置：\n * Spring Boot配置：resources/application.yaml是Spring Boot的配置文件，如服务名、数据库配置等\n * 日志配置：我们使用了默认的logback作为日志系统，配置在resources/logback-spring.xml中\n* 测试用例：test下，与client和common类似，不再赘述。\n\n上述分层看起来有些复杂，但会让各个层次的职责划分的更为清楚，如果你的项目中有更好的方案，也可以采用已有分层结构。\n\n## job子项目的代码结构\n\n最后，我们看一下job子项目的目录结构：\n\n```shell\n├── build.gradle\n└── src\n    └── main\n        ├── java\n        │   └── com\n        │       └── coder4\n        │           └── lmsia\n        │               └── abc\n        │                   └── job\n        │                       ├── LmsiaAbcJob.java\n        │                       └── LmsiaAbcJobStarter.java\n        └── resources\n            ├── application.yaml\n            └── logback-spring.xml\n\n```\n\n简单解释下：\n* 命令行入口: 本节开篇部分已经提到，job是可执行程序，LmsiaAbcJobStarter即是命令行的入口。\n* 具体job: 这里只有一个LmsiaAbcJob，会通过参数与入口关联，后续会详细讲解。\n\n至此，我们已经对lmsia这个示例项目的Gradle、子项目划分、子项目结构做了较为详尽的讲解。\n\n需要说明的是：由于篇幅先后关系的问题，server子项目我们并未包含数据库、事件处理的相关文件和目录结构，我们会在后续章节视进度逐渐添加。\n\n[^1]：数据来源自官方性能评测[Gradle vs Maven: Performance Comparison](https://gradle.org/gradle-vs-maven-performance/)\n"
  },
  {
    "path": "legacy/spring-boot/sb-mockito.md",
    "content": "# Mockito 单元测试打桩神器\n\n## 单元测试\n\n软件测试是软件质量保证的关健环节，代表了需求、设计和编码的最终检查。\n\n![软件测试金字塔](./test.jpg \"软件测试金字塔\")\n\n如上图所示，测试金字塔将测试分为三类\n* 单元测试: 由开发者自行编写，应当覆盖80%以上的场景。对微服务架构而言，主要是在单个微服务内部，对复杂业务逻辑，编写单元测试。\n* 集成测试: 由测试人员编写，强调系统整体联动，多偏向业务可用性验证。如下单流程是否畅通，库存扣减是否成功。它的覆盖场景一般之占10%。\n* 功能测试: 一些由单元测试、集成测试不好做的，通过功能测试完成，一般来说，这类不好自动化的，需要手动进行测试。由于测试成功很高，这类一般只覆盖5%的场景。\n\n测试金字塔也向我们揭示了一个实时：单元测试是整个测试环节的根基，如果单元测试做不好，上层的集成测试、功能测试都会无从谈起。\n\n遗憾的是，多数开发者都不具备编写单元测试的良好习惯，甚至缺乏编写单元的动力。\n\n除了缺乏软件质量保障的意识外，\"嫌麻烦\"也是这类开发者面对单元测试的口头禅。\n\n本节将介绍Mockito，这是一个单元测试的利器。Mockito的出现，让我们可以更加轻松地编写单元测试。\n\n在介绍Mockito之前，先来解释下，我们为什么不推荐使用Spring Boot启动单元测试框架。\n\n实际上，Spring Boot本身是提供了单元测试框架的，可以在JUnit中通过注解的配置，启动一个Spring上下文环境，并支持自动注入等功能，如果你感兴趣，可以参考[这篇文档](http://www.baeldung.com/spring-boot-testing)。\n\n在实际工作中，我也尝试过上述方法，但效果却并不太好，主要原因是：\n* 启动Spring Boot环境速度很慢，至少要3秒，而一般的单元测试都是毫秒级别。\n* 依赖管理需要手动声明，随着业务不断升级，经常忘记维护单元测试中的依赖，导致单元测试无法正常执行。\n\n基于上述原因，我强烈不推荐在单元测试中启动Spring Boot环境。\n\n对于服务之间存在依赖关系的场景，建议直接使用Mockito的打桩(Mock)进行。\n\n希望在仔细的阅读本节后，你也会爱上单元测试:-)\n\n## Mockito\n\n在软件测试中，Mock指的是效仿、模仿。Mockito就是为了解决测试中的Mock问题而诞生的，它可以很好的解决单元测试中，由于不同类耦合而带来的难以测试的问题。\n\n还是以上面Spring Boot环境为例子。假设我们要测试A类，而类A又调用了B类和C类。此时可能有两种选择：\n\n1. 手动构造B和C。\n1. 通过启动Spring环境，自动地注入B和C。\n\n现在有了Mockito后，我们有了另外的思路：无需构造B和C，而是通过Mockito，\"Mock\"出B和C(构造符合接口但没有实现的类)，由于我们要测试的是A类中的逻辑，只要检查A调用B和C的时机、次数、参数是否正确，就可以了。\n\n我们通过一个例子，来说明mockito的用法。\n\n首先是ServiceA和它的实现:\n```java\npackage com.coder4.lmsia.abc.server.service.intf;\n\n/**\n * @author coder4\n */\npublic interface ServiceA {\n\n    int methodA(int a, int b);\n\n}\n```\n\n```java\npackage com.coder4.lmsia.abc.server.service.impl;\n\nimport com.coder4.lmsia.abc.server.service.intf.ServiceA;\nimport com.coder4.lmsia.abc.server.service.intf.ServiceB;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\n\n/**\n * @author coder4\n */\n@Service\npublic class ServiceAImpl implements ServiceA {\n\n    @Autowired\n    private ServiceB serviceB;\n\n    @Override\n    public int methodA(int a, int b) {\n        if (a <= 10 && b <= 10) {\n            return a + b;\n        } else {\n            return serviceB.methodB(a, b);\n        }\n    }\n}\n```\n\n然后是服务B和它的实现:\n```java\npackage com.coder4.lmsia.abc.server.service.intf;\n\n/**\n * @author coder4\n */\npublic interface ServiceB {\n\n    int methodB(int a, int b);\n\n}\n```\n\n```java\npackage com.coder4.lmsia.abc.server.service.impl;\n\nimport com.coder4.lmsia.abc.server.service.intf.ServiceB;\nimport org.springframework.stereotype.Service;\n\n/**\n * @author coder4\n */\n@Service\npublic class ServiceBImpl implements ServiceB {\n\n    @Override\n    public int methodB(int a, int b) {\n        return a * b;\n    }\n}\n```\n\n我们总结一下功能：\n\n* 在服务A中，若参数a和b都小于10，则返回求和结果，否则交给服务B处理。\n* 在服务B中，直接返回参数a和b的乘积结果。\n\n在编写单元测试前，先要引用对应的包，lmabc-server/build.gradle：\n```grovvy\ndependencies {\n    compile project(':lmsia-abc-common')\n    ...\n    testCompile 'junit:junit:4.12'\n    testCompile 'org.mockito:mockito-all:1.9.5'\n}\n```\n\n这里要指出的是，mockito本身还是需要单元测试框架才能运行的，我们这里用的是最常见的JUnit。\n\n然后看一下单元测试\n```java\npackage com.coder4.lmsia.abc.server;\n\nimport com.coder4.lmsia.abc.server.service.impl.ServiceAImpl;\nimport com.coder4.lmsia.abc.server.service.intf.ServiceA;\nimport com.coder4.lmsia.abc.server.service.intf.ServiceB;\nimport org.junit.Assert;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.Mockito;\nimport org.mockito.internal.util.reflection.Whitebox;\n\nimport static org.hamcrest.CoreMatchers.is;\n\n/**\n * @author coder4\n */\npublic class ServiceATest {\n\n    private ServiceA serviceA;\n\n    private ServiceB serviceB;\n\n    @Before\n    public void before() {\n        serviceA = new ServiceAImpl();\n        serviceB = Mockito.mock(ServiceB.class);\n        Whitebox.setInternalState(serviceA, \"serviceB\", serviceB);\n    }\n\n    @Test\n    public void testBelow10() {\n        Assert.assertThat(serviceA.methodA(1, 1), is(2));\n\n        Mockito.verifyZeroInteractions(serviceB);\n\n    }\n\n    @Test\n    public void testAbove10() {\n        serviceA.methodA(100, 1);\n\n        Mockito.verify(serviceB).methodB(100, 1);\n\n    }\n\n}\n```\n\n我们分步解释一下：\n* before: 初始化ServiceA，因为我们要测试这个，所以必须手动初始化。而ServiceB我们在A的测试并不关注，直接Mock一个，并通过Whitebox注入到服务A中。\n* testBelow10: 前面服务实现已经介绍过，当参数a和b都小于10的场景，是在ServiceA中直接求和。所以这里我们验证两个的和，然后验证下是不是没有\"碰过\"服务B(verifyZeroInteractions)\n* testAbove10: 当任何一个参数大于10时候，实际会走服务B。所以我们验证下是否调用了服务B，且参数恰好是传给A的就好。\n\n怎么样，有了Mockito后，测试是不是变得有趣起来了:-)\n\n[官方文档](http://static.javadoc.io/org.mockito/mockito-core/2.18.3/org/mockito/Mockito.html)中提供了更多有趣的例子，等待你的发掘。\n"
  },
  {
    "path": "legacy/spring-boot/sb-rest.md",
    "content": "# Spring Boot REST接口\n\n在介绍服务发现和负载均衡时已经提到，我们的架构中，对每个微服务开放两个虚拟IP端口，一个是RPC，另外一个是REST(HTTP)。\n\n在上一节中，我们探讨了Spring Boot中集成Thrift RPC的方案，主要是针对RPC端口。\n\n在本节中，我们首先看一下优雅停机的问题，随后探讨REST服务的发现与负载均衡均衡的问题。\n\n## 优雅停机 \n\n如果你经历过生产环境架构设计的话，一定遇到过\"优雅停机\"的需求。\n\n优雅停机指的是在服务重启过程中，每个服务节点在不影响任何线上请求的前提下，有计划而平滑的退出。\n\n听起来有些抽象，我们看看下图中的例子：\n\n![优雅停机例子](./graceful-shutdown.png \"优雅停机例子\")\n\n1. Kubernetes VIP收到请求，假设分发到Service B的Replica 3(最下面的节点)上\n1. Replica 3的节点因计划升级，恰好停机，变为红色。\n1. 此时，之前分发到Replica 3上的请求没有被处理完，服务就被停机了，于是发生错误。\n\n你可能会说，这种概率非常小。但随着系统越来越庞大、上线节奏越来越快、业务规模越来越大后，出现这种问题的概率也会逐步增大。\n\n实际上，我们在[微服务的自动发现与负载均衡](../ms-discovery/msd.md)中所使用的例子就会有这个问题。\n\n我们来试验一下。首先，我们为镜像新建两个不同的版本0.1和0.2(实际内容是一样的)。\n\n然后登录minikube，执行如下命令：\n\n```shell\nwhile true; do curl -s \"http://10.97.42.195:8080/lmsia-abc/api/\" > /dev/null || echo \"curl fail\" ;done\n```\n\n上述命令，会curl不间断地通过虚拟IP访问REST服务，如果出错的时候会报警。如果一切正常的话，当你执行上述命令，不会出现\"curl fail\"的报警。\n\n下面让我们修改一下yaml文件：\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: lmsia-abc-server-deployment\nspec:\n  selector:\n    matchLabels:\n      app: lmsia-abc-server\n  replicas: 2\n  template:\n    metadata:\n      labels:\n        app: lmsia-abc-server\n    spec:\n      containers:\n      - name: lmsia-abc-server-ct\n        # here change from latest to 0.1\n        image: coder4/lmsia-abc-server:0.1\n        ports:\n        - containerPort: 8080\n        - containerPort: 3000\n\n```\n\n上面我们修改了镜像的版本到0.1，然后我们新打开一个shell，应用一下\n```shell\nkubectl apply -f ./lmsia-abc-server-deployment.yaml\n```\n\n再切换回刚才执行循环curl的命令行，会发现大量的失败：\n```shell\n...\ncurl fail\ncurl fail\ncurl fail\ncurl fail\n....\n```\n\n这就是说明，在Kubernetes更新Pod的镜像版本的时候，但是还是有部分请求打到了被杀掉的Pod上，从而导致请求失败。或者说，发生了\"不优雅\"的停机。\n\n为了解决这个问题，我们可以假设一种如下的方案：\n1. Replica 3节点需要停机前，以某种方式通知Kubernetes的VIP\n1. Kubernetes的VIP收到通知后，摘掉Replica 3\n1. Replica 3停机。\n\n上述方案比起\"粗鲁\"的直接杀掉Replica 3，要\"优雅\"地多。实际上，这也是大多数系统中，优雅停机方案的原型。\n\n我们来看一下具体的原理，首先看看deployment.yaml：\n```shell\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: lmsia-abc-server-deployment\nspec:\n  selector:\n    matchLabels:\n      app: lmsia-abc-server\n  replicas: 2\n  template:\n    metadata:\n      labels:\n        app: lmsia-abc-server\n    spec:\n      containers:\n      - name: lmsia-abc-server-ct\n        image: coder4/lmsia-abc-server:0.2\n        ports:\n        - containerPort: 8080\n        - containerPort: 3000\n        readinessProbe:\n          httpGet:\n            path: /health\n            port: 8081\n          initialDelaySeconds: 5\n          periodSeconds: 5\n\n```\n\n与之前的deployment定义相比，这里新增了\"readinessProbe\"一项目。关于它的详细解释可以参考官方文档，这里简单解释下具体的这个例子。\n* Kubernetes会每间隔5秒钟，通过HTTP协议请求8081端口下的/health路径。\n* 若能成功打开，会认为服务可用，不做任何处理。\n* 若不能打开(非200返回值或无法连接)则会将对应的Pod从VIP上摘除，直到恢复为可用。\n\n对应地，在lmsia-abc服务中，我们也应该定义这个8081端口的服务。\n\n在本书架构中，我们使用Spring Boot内置的health indicator，即健康监控机制。\n\n首先是自动配置GracefulShutdownConfiguration：\n\n```java\npackage com.coder4.lmsia.gracefulshutdown.configuration;\n\nimport com.coder4.lmsia.gracefulshutdown.GracefulStatusHealthIndicator;\nimport org.springframework.beans.factory.DisposableBean;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n/**\n * @author coder4\n */\n@Configuration\npublic class GracefulShutdownConfiguration implements DisposableBean {\n\n    private static final int GRACE_SHUTDOWN_MS = 6000;\n\n    private GracefulStatusHealthIndicator gshIndicator = new GracefulStatusHealthIndicator();\n\n    @Bean\n    @ConditionalOnMissingBean(GracefulStatusHealthIndicator.class)\n    public GracefulStatusHealthIndicator gracefulStatusHealthIndicator() {\n        return gshIndicator;\n    }\n\n    @Autowired\n    @Qualifier(\"shutdownThriftServerRunnable\")\n    private Runnable shutdownThriftServerRunnable;\n\n    @Override\n    public void destroy() throws Exception {\n        gshIndicator.setReady(false);\n        Thread.sleep(GRACE_SHUTDOWN_MS);\n        if (shutdownThriftServerRunnable != null) {\n            shutdownThriftServerRunnable.run();\n        }\n    }\n}\n```\n\n如上所示\n* 自动配置会无条件激活。\n* 初始状态，新建一个GracefulStatusHealthIndicator，我们稍后会讲解它。\n* 销毁时，设置indicator的状态为not ready，睡眠6秒，然后尝试关闭Thrift服务。\n\n为什么这里要睡眠6秒钟呢？我们可以回顾下，刚才设置deployment的时候，Kubernetes是每间隔5秒钟扫描一次。所以这里设置的休眠6秒钟，刚好可以覆盖一个扫描周期。\n\n然后是具体的健康监控GracefulStatusHealthIndicator：\n```java\npackage com.coder4.lmsia.gracefulshutdown;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.boot.actuate.health.Health;\nimport org.springframework.boot.actuate.health.HealthIndicator;\n\n/**\n * @author coder4\n */\npublic class GracefulStatusHealthIndicator implements HealthIndicator {\n\n    private static final String GRACEFUL_STATUS_KEY = \"graceful_status\";\n\n    private Logger LOG = LoggerFactory.getLogger(getClass());\n\n    private Health health;\n\n    public GracefulStatusHealthIndicator() {\n        setReady(true);\n    }\n\n    public void setReady(boolean ready) {\n        synchronized (this) {\n            if (ready) {\n                health = new Health.Builder().withDetail(GRACEFUL_STATUS_KEY, \"graceful_status_up\").up().build();\n                LOG.info(\"graceful_status up\");\n            } else {\n                health = new Health.Builder().withDetail(GRACEFUL_STATUS_KEY, \"graceful_status_down\").down().build();\n                LOG.info(\"graceful_status down\");\n            }\n        }\n    }\n\n    @Override\n    public Health health() {\n        return health;\n    }\n}\n```\n\n上述代码比较简单，就是利用了Spring Boot内置的HealIndicator，当ready = false时，indicator和health都会挂掉(down)，导致对应健康监控服务不可用。\n\n最后，我们如何把健康监控导到8081端口呢？答案是通过application.yaml里的配置：\n```\n... other config ...\nmanagement:\n    port: 8081\n    security.enabled: false\n... other config ...\n```\n\n熟悉Spring Boot的朋友清楚，这里如果不做上述定义，那么默认会在8080端口打开上述监控。为什么还要多配置这一步呢？这主要是为了安全性的考量。在实际开发中，可能还会暴露CPU、内存、硬盘等信息，而这些对于安全渗透人员来讲，都是非常危险的内部信息。因此，我们将这些健康信息放在另外的端口8081，并且不配置Nginx的反向代理，让他与外网隔离。\n\n配置好这些后，你可以再执行循环curl的测试，并且升级镜像\"coder4/lmsia-abc-server\"到0.2，可以发现curl的访问再没有中断过。这就说明，我们的\"优雅停机\"配置成功了！\n\n## REST(HTTP)服务的发现与负载均衡\n\n如果大家仔细阅读了[Spring Boot整合Thrift RPC](spring-boot-1/sb-thrift.md)，并且认真思考一下后，不难想到，其实REST(HTTP)服务的发现与负载均衡，与RPC服务的负载均衡，并没有什么两样。\n\n![REST(HTTP)服务的发现与负载均衡](./rest-nginx.png \"REST(HTTP)服务的发现与负载均衡\")\n\n如上图所示\n* 在我们的架构中，REST(HTTP)端口直接面向Web、PC、移动客户端暴露，所以在最外层，部署了一层Nginx，作为接入网关的代理。在Nginx上，通过UpStream的方式，指向对应微服务的VIP的Host地址和端口8080。\n* 我们的REST(HTTP)服务集成及负载均衡，也采用Kubenetes的Service和虚拟IP来实现，唯一的区别是端口为8080。\n\n需要特别指出的是，之前在讨论RPC的负载据均衡和发现时，我们并没有列出外部用户。实际上，在我们的架构中，RPC服务只面向内部使用，即只能是其他内部微服务调用，而不对外暴露服务能力。这主要是基于安全性的考虑。\n\n有了上述一层隔离后，我们可以采取如下策略：\n* 由于微服务的RPC只对内部可用，可以跳过基本的鉴权等安全性检查，从而提高系统性能、降低开发难度。\n* 在对外部暴露的REST(HTTP)服务中，再进行鉴权等安全性检查。\n\n由于已经内置了Spring MVC，在Spring Boot中集成REST服务非常简单，我们来看一个例子：\n```java\npackage com.coder4.lmsia.abc.server.rest.controller;\n\nimport com.coder4.lmsia.abc.constant.LmsiaAbcConstant;\nimport com.coder4.lmsia.abc.server.rest.logic.intf.AbcLogic;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n/**\n * @author coder4\n */\n@RestController\n@RequestMapping(LmsiaAbcConstant.REST_API)\npublic class AbcController {\n\n    @Autowired\n    private AbcLogic abcLogic;\n\n    @GetMapping(value = \"/\")\n    public String hello() {\n        return abcLogic.getHello();\n    }\n\n}\n```\n\n再看下这个常量：\n```java\npublic class LmsiaAbcConstant {\n\n    ...\n    public static final String REST_API = \"/lmsia-abc/{client:api|iphone|ipad|android|win}\";\n    ...\n\n}\n```\n\n如上所述，我们通过@RestController和RequestMapping，设定了REST服务的路径为：\"http://host:8080/lmsia-abc/api/，为什么要加上lmsia-abc这一层路径呢？\n\n这是为了配置Nginx的UpStream时方便。加上这一层后，我们可以直接将端口转发过来，而不需要再纠结路径层面的转发。只要微服务的命名规则约定好，就能保证微服务之间的REST服务的路径不会相互冲突。\n\n至此，我们已经完成了RPC服务、REST服务在Spring Boot中的集成，并且都实现了服务发现、负载均衡的这微服务的核心功能。\n"
  },
  {
    "path": "legacy/spring-boot/sb-thrift.md",
    "content": "# Spring Boot整合Thrift RPC\n\n## Spring Boot自动配置简介\n\n在介绍RPC之前，我们先来学习下Spring Boot的自动配置。\n\n我们前面已经提到：Spring Boot来源于Spring，并且做了众多改进，其中最有用的设计理念是约定优于配置，它通过自动配置功能（大多数开发者平时习惯设置的配置作为默认配置）为开发者快速、准确地构建出标准化的应用。\n\n以集成MySQL数据库为例，在Spring Boot出现之前，我们要\n1. 配置JDBC驱动依赖\n1. 配置XML文件中数据源\n1. 配置XML中的DataSource Bean\n1. 配置XML中的XXXTemplate Bean\n1. 配置XML中的XXXTransactionManager Bean\n\n有了Spring Boot的自动配置后，自动配置帮我们生成了各种DataSource、XXXTemplate、XXXTransactionManager，我们所需要做的只有一条，就是激活它\n1. maven中依赖包含自动配置的包\n1. 配置JDBC驱动依赖\n1. yaml文件中定义数据源\n\n自动配置进行智能检测，只要满足上述3个条件，其他的Bean都会被自动生成并注入到Spring环境中。我们需要使用时只需要@Autowired一下就可以了，是不是非常简单！\n\n由于篇幅所限，本书不会对自动配置的书写做零起点教学，如果你想了解自动配置的原理，可以参考这篇文章[spring boot实战(第十三篇)自动配置原理分析](https://blog.csdn.net/liaokailin/article/details/49559951)\n\n在本节的后续部分，我们会以Thrift RPC Server为例，看看自动配置是如何书写的。\n\n## RPC简介\n\n远程过程调用(remote procedure call或简称RPC)，指的是运行于本地(客户端)的程序像调用本地程序一样，直接调用另一台计算机(服务器端)的程序，而程序员无需额外为远程交互做额外的编程。\n\nRPC极大地简化了分布似乎系统中节点之间网络通信的开发工作量，是微服务架构中的重要组件之一。\n\n在本书中，我们选用Thrift作为RPC框架。由于篇幅所限，我们不会对Thrift RPC作出详尽的介绍，如果你还不熟悉，可以参考官方的[快速入门文档](https://thrift.apache.org/tutorial/java)。\n\n\n## Spring Boot整合Thrift RPC服务端\n\n简要来说，启动一个Thrift RPC的服务端需要如下步骤:\n1. 书写DSL(.thrift文件)，定义函数、数据结构等。\n1. 编译并生成桩代码。\n1. 编写Handler(RPC的逻辑入口)。\n1. 基于上述Handler，构造Processor。\n1. 构造Server，Thrift提供了多种服务端供选择，常用的有TThreadPoolServer(多线程服务器)和TNonblockingServer(非阻塞服务器)。\n1. 设置Server的Protocol，类似的，Thrift提供了多种传输协议，最常用的是TBinaryProtocol和TCompactProtocol。\n1. 设置Server的Transport(Factory)，用这种方式指定底层的传输协议，常用的有TFramedTransport、TNonBlockingTransport，不同的Transport可以类似Java的IOStreawm方式，相互叠加，以产生更强大的效果。\n\n上述对Thrift服务器的架构做了简要介绍，如果想更深入了解，可以自行阅读[官方源码](https://github.com/apache/thrift/tree/master/lib/java/src/org/apache/thrift)。\n\n首先，我们来看一下thrift定义(根据上一节的介绍，文件放在lmsia-abc-common包中)\n```thrift\nnamespace java com.coder4.lmsia.abc\n\nservice lmsiaAbcThrift {\n\n    string sayHi()\n}\n```\n\n调用thrift进行编译后，我们也将对应的桩文件放置在lmsia-abc-client下，目录结构可以参见上一节。\n\n为了更方便的在Spring Boot中集成Thrift服务器，我将相应代码抽取成了公用库[lmsia-thrift-server](https://github.com/liheyuan/lmsia-thrift-server)\n```shell\n\n├── build.gradle\n├── gradle\n│   └── wrapper\n│       ├── gradle-wrapper.jar\n│       └── gradle-wrapper.properties\n├── gradlew\n├── gradlew.bat\n├── README.md\n├── settings.gradle\n└── src\n    ├── main\n    │   ├── java\n    │   │   └── com\n    │   │       └── coder4\n    │   │           └── lmsia\n    │   │               └── thrift\n    │   │                   └── server\n    │   │                       ├── configuration\n    │   │                       │   └── ThriftServerConfiguration.java\n    │   │                       └── ThriftServerRunnable.java\n    │   └── resources\n    │       └── META-INF\n    │           └── spring.factories\n    └── test\n        └── java\n\n```\n\n简单解析下项目结构：\ngradle相关: 与前节介绍的类似，只不过这里是单项目功能。\nThriftServerConfiguration: 自动配置，当满足条件后会自动激活，激活后可自动启动Thrift RPC服务。\nThriftServerRunnable: Thrift RPC服务器的构造逻辑、运行线程。 \nspring.factories: 当我们以类库方式提供自动配置时，需要增加这个spring.factories，让别的项目能\"定位到\"要检查的自动配置。\n\n首先，我们来看一下ThriftServerRunnable.java\n```java\n\npackage com.coder4.lmsia.thrift.server;\n\nimport org.apache.thrift.TProcessor;\nimport org.apache.thrift.protocol.TBinaryProtocol;\nimport org.apache.thrift.protocol.TProtocolFactory;\nimport org.apache.thrift.server.TServer;\nimport org.apache.thrift.server.TThreadedSelectorServer;\nimport org.apache.thrift.transport.TFramedTransport;\nimport org.apache.thrift.transport.TNonblockingServerSocket;\nimport org.apache.thrift.transport.TNonblockingServerTransport;\nimport org.apache.thrift.transport.TTransportException;\nimport org.apache.thrift.transport.TTransportFactory;\n\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.SynchronousQueue;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * @author coder4\n */\npublic class ThriftServerRunnable implements Runnable {\n\n    private static final int THRIFT_PORT = 3000;\n\n    private static final int THRIFT_TIMEOUT = 5000;\n\n    private static final int THRIFT_TCP_BACKLOG = 5000;\n\n    private static final int THRIFT_CORE_THREADS = 128;\n\n    private static final int THRIFT_MAX_THREADS = 256;\n\n    private static final int THRIFT_SELECTOR_THREADS = 16;\n\n    private static final TProtocolFactory THRIFT_PROTOCOL_FACTORY = new TBinaryProtocol.Factory();\n\n    // 16MB\n    private static final int THRIFT_MAX_FRAME_SIZE = 16 * 1024 * 1024;\n\n    // 4MB\n    private static final int THRIFT_MAX_READ_BUF_SIZE = 4 * 1024 * 1024;\n\n    protected ExecutorService threadPool;\n\n    protected TServer server;\n\n    protected Thread thread;\n\n    private TProcessor processor;\n\n    private boolean isDestroy = false;\n\n    public ThriftServerRunnable(TProcessor processor) {\n        this.processor = processor;\n    }\n\n    public TServer build() throws TTransportException {\n        TNonblockingServerSocket.NonblockingAbstractServerSocketArgs socketArgs =\n                new TNonblockingServerSocket.NonblockingAbstractServerSocketArgs();\n        socketArgs.port(THRIFT_PORT);\n        socketArgs.clientTimeout(THRIFT_TIMEOUT);\n        socketArgs.backlog(THRIFT_TCP_BACKLOG);\n\n        TNonblockingServerTransport transport = new TNonblockingServerSocket(socketArgs);\n\n        threadPool =\n                new ThreadPoolExecutor(THRIFT_CORE_THREADS, THRIFT_MAX_THREADS,\n                        60L, TimeUnit.SECONDS,\n                        new SynchronousQueue<>());\n\n        TTransportFactory transportFactory = new TFramedTransport.Factory(THRIFT_MAX_FRAME_SIZE);\n        TThreadedSelectorServer.Args args = new TThreadedSelectorServer.Args(transport)\n                .selectorThreads(THRIFT_SELECTOR_THREADS)\n                .executorService(threadPool)\n                .transportFactory(transportFactory)\n                .inputProtocolFactory(THRIFT_PROTOCOL_FACTORY)\n                .outputProtocolFactory(THRIFT_PROTOCOL_FACTORY)\n                .processor(processor);\n\n        args.maxReadBufferBytes = THRIFT_MAX_READ_BUF_SIZE;\n\n        return new TThreadedSelectorServer(args);\n    }\n\n    @Override\n    public void run() {\n        try {\n            server = build();\n            server.serve();\n        } catch (Exception e) {\n            e.printStackTrace();\n            throw new RuntimeException(\"Start Thrift RPC Server Exception\");\n        }\n    }\n\n    public void stop() throws Exception {\n        threadPool.shutdown();\n        server.stop();\n    }\n\n}\n\n```\n\n我们来解释一下：\n* build方法用于构造一个可供运行的Thrift RPC Server\n 1. 构造非阻塞Socket，并设置监听端口、超时\n 2. 构造非阻塞Transport\n 3. 构造线程池，在这里我们的服务器模型是非阻塞线程池RPC服务器。\n 4. 构造底层传输协议即TFramedTransport\n 5. 构造ThriftServer，并设置前面构造的非阻塞Transport、线程池、协议TBinaryProtocol\n* 整个ThriftServerRunnable类是一个线程Runnablerun，run函数中构造RPC服务，并启动服务(servee)\n* stop服务提供停止服务的方法\n\n下面我们来看一下自动配置ThriftServerConfiguration.java：\n```java\npackage com.coder4.lmsia.thrift.server.configuration;\n\nimport com.coder4.lmsia.thrift.server.ThriftServerRunnable;\nimport org.apache.thrift.TProcessor;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.DisposableBean;\nimport org.springframework.beans.factory.InitializingBean;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnBean;\nimport org.springframework.context.annotation.Configuration;\n\nimport java.util.concurrent.TimeUnit;\n\n/**\n * @author coder4\n */\n@Configuration\n@ConditionalOnBean(value = {TProcessor.class})\npublic class ThriftServerConfiguration implements InitializingBean, DisposableBean {\n\n    private Logger LOG = LoggerFactory.getLogger(ThriftServerConfiguration.class);\n\n    private static final int GRACEFUL_SHOWDOWN_SEC = 3;\n\n    @Autowired\n    private TProcessor processor;\n\n    private ThriftServerRunnable thriftServer;\n\n    private Thread thread;\n\n    @Override\n    public void destroy() throws Exception {\n        LOG.info(\"Wait for graceful shutdown on destroy(), {} seconds\", GRACEFUL_SHOWDOWN_SEC);\n        Thread.sleep(TimeUnit.SECONDS.toMillis(GRACEFUL_SHOWDOWN_SEC));\n        LOG.info(\"Shutdown rpc server.\");\n        thriftServer.stop();\n        thread.join();\n    }\n\n    @Override\n    public void afterPropertiesSet() throws Exception {\n        thriftServer = new ThriftServerRunnable(processor);\n        thread = new Thread(thriftServer);\n        thread.start();\n    }\n}\n\n```\n\n这是我们编写的第一个自动配置，我们稍微详细的解释一下：\n* 启动条件: 仅当服务提供了TProcessor才启用，我们稍后会在lmsia-abc项目中看到，后者封装了RPC的桩入口，提供了TProcessor。\n* InitializingBean: 自动配置实现了InitializingBean，为什么要实现这个接口呢？当这个自动配置被初始化时，所有Autowired的属性被自动注入（即Processor），而前面ThriftServerRunnable中我么已经看到，只有拿到了TProcessor，才能启动RPC服务。因此，我们使用了InitializingBean，它自带了afterPropertiesSet这个回调，会在所有属性被注入完成后，调用这个回调函数。\n * 在这里，我们调用了ThriftServerRunnable实现了Thrift RPC服务器的启动。\n* DisposableBean: 除了InitializingBean，我们还实现了DisposableBean。看名字就可以知道，这是Spring为了服务关闭时清理资源而设计的接口。事实也是如此，当服务关闭时，会依次调用每个自动配置，如果实现了DisposableBean，则回调destroy函数。\n * 在这里，我们先让线程休眠3秒，然后才关闭Thrift RPC服务，这主要是为了Graceful Shutdown而设计的(\"优雅关闭\")，关于这一点，我们会在下一节会做详细讲解。\n\n最后，我们的自动配置默认是无法被发现的，需要一个配置文件spring.factories：\n```shell\norg.springframework.boot.autoconfigure.EnableAutoConfiguration=com.coder4.lmsia.thrift.server.configuration.ThriftServerConfiguration\n\n```\n\n解读完lmsia-thrift-server后，我们看看如何将它整合进lmsia-abc项目中。\n\n1. 在lmsia-abc-server子项目中的build.gradle中加入：\n```grovvy\ncompile 'com.github.liheyuan:lmsia-thrift-server:0.0.1'\n```\n\n1. 提供一个TProcessor，如前文所述，这是启用自动配置的必要条件，ThriftProcessorConfiguration:\n```java\npackage com.coder4.lmsia.abc.server.configuration;\n\nimport com.coder4.lmsia.abc.thrift.LmsiaAbcThrift;\nimport com.coder4.lmsia.abc.server.thrift.ThriftServerHandler;\nimport org.apache.thrift.TProcessor;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n/**\n * @author coder4\n */\n@Configuration\n@ConditionalOnProperty(name = \"thriftServer.enabled\", matchIfMissing = true)\npublic class ThriftProcessorConfiguration {\n\n    @Bean(name = \"thriftProcessor\")\n    public TProcessor processor(ThriftServerHandler handler) {\n        return new LmsiaAbcThrift.Processor(handler);\n    }\n\n}\n\n``` \n\n我们简单解释下：\n* 这也是一个自动配置，仅当配置文件中thriftServer.enabled=true时才启用(不配置默认true)\n* 提供的TProcessor，需要依赖ThriftServerHandler，这个就是Thrift生成的桩函数，项目结构分析中已经提到过，这是RPC服务器的逻辑入口。\n\n怎么样，使用了自动配置后，启动一个Thrift 服务器是不是非常简单？\n\n## Spring Boot整合Thrift RPC客户端\n\n只有服务端是不行的，还需要有客户端。\n\n类似地，为了方便的生成客户端，我们把代码进行了整理和抽象，放到了[lmsia-thrift-client](https://github.com/liheyuan/lmsia-thrift-client)项目中。\n\n首先看一下项目结构：\n```shell\n├── build.gradle\n├── gradle\n│   └── wrapper\n│       ├── gradle-wrapper.jar\n│       └── gradle-wrapper.properties\n├── gradlew\n├── gradlew.bat\n├── README.md\n├── settings.gradle\n└── src\n    ├── main\n    │   ├── java\n    │   │   └── com\n    │   │       └── coder4\n    │   │           └── lmsia\n    │   │               └── thrift\n    │   │                   └── client\n    │   │                       ├── ThriftClient.java\n    │   │                       ├── AbstractThriftClient.java\n    │   │                       ├── EasyThriftClient.java\n    │   │                       ├── K8ServiceThriftClient.java\n    │   │                       ├── K8ServiceKey.java\n    │   │                       ├── builder\n    │   │                       │   ├── EasyThriftClientBuilder.java\n    │   │                       │   └── K8ServiceThriftClientBuilder.java\n    │   │                       ├── func\n    │   │                       │   ├── ThriftCallFunc.java\n    │   │                       │   └── ThriftExecFunc.java\n    │   │                       ├── pool\n    │   │                       │   ├── TTransportPoolFactory.java\n    │   │                       │   └── TTransportPool.java\n    │   │                       └── utils\n    │   │                           └── ThriftUrlStr.java\n    │   └── resources\n    └── test\n        └── java\n            └── LibraryTest.java\n\n\n```\n\n解释下项目结构：\n* gradle相关的与之前类似，不再赘述\n* ThriftClient相关，定义了Thrift的客户端\n 1. ThriftClient 抽象了客户端的接口\n 1. AbstractThriftClient 实现了除连接外的Thrift Client操作\n 1. EasyThriftClient 使用IP和端口直连的Thrift Client\n 1. K8ServiceThriftClient 使用Kubernetes服务名字(根据[微服务自动发现](../ms-discovery/msd.md)一节中的介绍，服务名字实际也是Host)和端口的Thrift Client，并内置了连接池。\n* func 函数编程工具类\n* builder 方便快速构造上述两种Thrift Client\n* pool 客户端连接池\n\n本小节主要对IP、端口直连的客户端即EasyThriftClient进行介绍。关于支持服务自动发现以及连接池功能的K8ServiceThriftClient，将在下一节进行介绍。\n\n先看一下接口定义，ThriftClient:\n```java\npackage com.coder4.lmsia.thrift.client;\n\nimport com.coder4.lmsia.thrift.client.func.ThriftCallFunc;\nimport com.coder4.lmsia.thrift.client.func.ThriftExecFunc;\nimport org.apache.thrift.TServiceClient;\n\nimport java.util.concurrent.Future;\n\n/**\n * @author coder4\n */\npublic interface ThriftClient<TCLIENT extends TServiceClient> {\n\n    /**\n     * sync call with return value\n     * @param tcall thrift rpc client call\n     * @param <TRET> return type\n     * @return\n     */\n    <TRET> TRET call(ThriftCallFunc<TCLIENT, TRET> tcall);\n\n    /**\n     * sync call without return value\n     * @param texec thrift rpc client\n     */\n    void exec(ThriftExecFunc<TCLIENT> texec);\n\n    /**\n     * async call with return value\n     * @param tcall thrift rpc client call\n     * @param <TRET>\n     * @return\n     */\n    <TRET> Future<TRET> asyncCall(ThriftCallFunc<TCLIENT, TRET> tcall);\n\n\n    /**\n     * asnyc call without return value\n     * @param texec thrift rpc client call\n     */\n    <TRET> Future<?> asyncExec(ThriftExecFunc<TCLIENT> texec);\n\n}\n```\n\n这里需要解释一下，上述实际分成了两大类:\n* exec 无返回值的rpc调用\n* call 有返回值的调用\n\n这里使用了Java 8的函数式编程进行抽象。如果不太熟悉的朋友，可以自行查阅相关资料。\n\n在函数式编程的帮助下，我们可以将每一个rpc调用都分为同步和异步两种，异步的调用会返回一个Future。\n\n再来看一下AbstractThriftClient:\n```java\n/**\n * @(#)AbstractThriftClient.java, Aug 01, 2017.\n * <p>\n * Copyright 2017 fenbi.com. All rights reserved.\n * FENBI.COM PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.\n */\npackage com.coder4.lmsia.thrift.client;\n\nimport com.coder4.lmsia.thrift.client.func.ThriftCallFunc;\nimport com.coder4.lmsia.thrift.client.func.ThriftExecFunc;\nimport org.apache.thrift.TServiceClient;\nimport org.apache.thrift.TServiceClientFactory;\nimport org.apache.thrift.protocol.TBinaryProtocol;\nimport org.apache.thrift.protocol.TProtocol;\nimport org.apache.thrift.transport.TTransport;\n\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.LinkedBlockingDeque;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * @author coder4\n */\npublic abstract class AbstractThriftClient<TCLIENT extends TServiceClient> implements ThriftClient<TCLIENT> {\n\n    protected static final int THRIFT_CLIENT_DEFAULT_TIMEOUT = 5000;\n\n    protected static final int THRIFT_CLIENT_DEFAULT_MAX_FRAME_SIZE = 1024 * 1024 * 16;\n\n    private Class<?> thriftClass;\n\n    private static final TBinaryProtocol.Factory protocolFactory = new TBinaryProtocol.Factory();\n\n    private TServiceClientFactory<TCLIENT> clientFactory;\n\n    // For async call\n    private ExecutorService threadPool;\n\n    public void init() {\n        try {\n            clientFactory = getThriftClientFactoryClass().newInstance();\n        } catch (Exception e) {\n            throw new RuntimeException();\n        }\n\n        if (!check()) {\n            throw new RuntimeException(\"Client config failed check!\");\n        }\n\n        threadPool = new ThreadPoolExecutor(\n                10, 100, 0,\n                TimeUnit.MICROSECONDS, new LinkedBlockingDeque<>());\n    }\n\n    protected boolean check() {\n        if (thriftClass == null) {\n            return false;\n        }\n        return true;\n    }\n\n    @Override\n    public <TRET> Future<TRET> asyncCall(ThriftCallFunc<TCLIENT, TRET> tcall) {\n        return threadPool.submit(() -> this.call(tcall));\n    }\n\n    @Override\n    public <TRET> Future<?> asyncExec(ThriftExecFunc<TCLIENT> texec) {\n        return threadPool.submit(() -> this.exec(texec));\n    }\n\n    protected TCLIENT createClient(TTransport transport) throws Exception {\n        // Step 1: get TProtocol\n        TProtocol protocol = protocolFactory.getProtocol(transport);\n\n        // Step 2: get client\n        return clientFactory.getClient(protocol);\n    }\n\n    private Class<TServiceClientFactory<TCLIENT>> getThriftClientFactoryClass() {\n        Class<TCLIENT> clientClazz = getThriftClientClass();\n        if (clientClazz == null) {\n            return null;\n        }\n        for (Class<?> clazz : clientClazz.getDeclaredClasses()) {\n            if (TServiceClientFactory.class.isAssignableFrom(clazz)) {\n                return (Class<TServiceClientFactory<TCLIENT>>) clazz;\n            }\n        }\n        return null;\n    }\n\n    private Class<TCLIENT> getThriftClientClass() {\n        for (Class<?> clazz : thriftClass.getDeclaredClasses()) {\n            if (TServiceClient.class.isAssignableFrom(clazz)) {\n                return (Class<TCLIENT>) clazz;\n            }\n        }\n        return null;\n    }\n\n    public void setThriftClass(Class<?> thriftClass) {\n        this.thriftClass = thriftClass;\n    }\n}\n```\n\n上述抽象的Thrift客户端实现了如下功能：\n1. 客户端线程池，这里主要是为异步调用准备的，与之前构造的服务端的线程池是完全不同的。\n * asyncCall和asyncExec使用了线程池来完成异步调用\n1. thriftClass 存储了Thrift的桩代码了类，不同业务生成的ThriftClass不一样，所以这里存储了class。\n1. createClient提供了共用函数，传入一个transport，即可构造生成一个Thrift Client，特别注意的是，这里设定的通信协议为TBinaryProtocol，必须与服务端保持一致，否则无法成功通信。\n\n由于call和exec与连接实现较为相关，因此并未在这一层中实现，最后我们来看一下EasyThriftClient:\n```java\npackage com.coder4.lmsia.thrift.client;\n\nimport com.coder4.lmsia.thrift.client.func.ThriftCallFunc;\nimport com.coder4.lmsia.thrift.client.func.ThriftExecFunc;\nimport org.apache.thrift.TServiceClient;\nimport org.apache.thrift.transport.TFramedTransport;\nimport org.apache.thrift.transport.TSocket;\nimport org.apache.thrift.transport.TTransport;\n\n/**\n * @author coder4\n */\npublic class EasyThriftClient<TCLIENT extends TServiceClient> extends AbstractThriftClient<TCLIENT> {\n\n    private static final int EASY_THRIFT_BUFFER_SIZE = 1024 * 16;\n\n    protected String thriftServerHost;\n\n    protected int thriftServerPort;\n\n    @Override\n    protected boolean check() {\n        if (thriftServerHost == null || thriftServerHost.isEmpty()) {\n            return false;\n        }\n        if (thriftServerPort <= 0) {\n            return false;\n        }\n        return super.check();\n    }\n\n    private TTransport borrowTransport() throws Exception {\n        TSocket socket = new TSocket(thriftServerHost, thriftServerPort, THRIFT_CLIENT_DEFAULT_TIMEOUT);\n\n        TTransport transport = new TFramedTransport(\n                socket, THRIFT_CLIENT_DEFAULT_MAX_FRAME_SIZE);\n\n        transport.open();\n\n        return transport;\n    }\n\n    private void returnTransport(TTransport transport) {\n        if (transport != null && transport.isOpen()) {\n            transport.close();\n        }\n    }\n\n    private void returnBrokenTransport(TTransport transport) {\n        if (transport != null && transport.isOpen()) {\n            transport.close();\n        }\n    }\n\n    @Override\n    public <TRET> TRET call(ThriftCallFunc<TCLIENT, TRET> tcall) {\n\n        // Step 1: get TTransport\n        TTransport tpt = null;\n        try {\n            tpt = borrowTransport();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n\n        // Step 2: get client & call\n        try {\n            TCLIENT tcli = createClient(tpt);\n            TRET ret = tcall.call(tcli);\n            returnTransport(tpt);\n            return ret;\n        } catch (Exception e) {\n            returnBrokenTransport(tpt);\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public void exec(ThriftExecFunc<TCLIENT> texec) {\n        // Step 1: get TTransport\n        TTransport tpt = null;\n        try {\n            tpt = borrowTransport();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n\n        // Step 2: get client & exec\n        try {\n            TCLIENT tcli = createClient(tpt);\n            texec.exec(tcli);\n            returnTransport(tpt);\n        } catch (Exception e) {\n            returnBrokenTransport(tpt);\n            throw new RuntimeException(e);\n        }\n    }\n\n    public String getThriftServerHost() {\n        return thriftServerHost;\n    }\n\n    public void setThriftServerHost(String thriftServerHost) {\n        this.thriftServerHost = thriftServerHost;\n    }\n\n    public int getThriftServerPort() {\n        return thriftServerPort;\n    }\n\n    public void setThriftServerPort(int thriftServerPort) {\n        this.thriftServerPort = thriftServerPort;\n    }\n\n```\n\n简单解释下上述代码\n1. 需要外部传入RPC服务器的主机名和端口 thriftServerHost和thriftServerPort\n1. borrowTransport完成Transport(Thrift中类似Socket的抽象) 的构造，注意这里要使用TFramedTransport，与之前服务端的构造保持一致。\n1. returnTransport关闭Transport\n1. returnBrokenTransport关闭出异常的Transport\n1. call和exec 在拿到Transport后，使用函数式编程的方式，完成rpc调用，如果有异常则关闭连接。\n\n最后我们来看一下对应的Builder，EasyThriftClientBuilder:\n```java\npackage com.coder4.lmsia.thrift.client.builder;\n\nimport com.coder4.lmsia.thrift.client.EasyThriftClient;\nimport org.apache.thrift.TServiceClient;\n\n/**\n * @author coder4\n */\npublic class EasyThriftClientBuilder<TCLIENT extends TServiceClient> {\n\n    private final EasyThriftClient<TCLIENT> client = new EasyThriftClient<>();\n\n    protected EasyThriftClient<TCLIENT> build() {\n        client.init();\n        return client;\n    }\n\n    protected EasyThriftClientBuilder<TCLIENT> setHost(String host) {\n        client.setThriftServerHost(host);\n        return this;\n    }\n\n    protected EasyThriftClientBuilder<TCLIENT> setPort(int port) {\n        client.setThriftServerPort(port);\n        return this;\n    }\n\n    protected EasyThriftClientBuilder<TCLIENT> setThriftClass(Class<?> thriftClass) {\n        client.setThriftClass(thriftClass);\n        return this;\n    }\n}\n\n```\n\nBuilder的代码比较简单，就是以链式调用的方式，通过主机和端口，方便地构造一个EasyThriftClient。\n\n看了EasyThriftClient后下面我们来看一下如何集成到项目中。\n\n在[Gradle子项目划分与微服务的代码结构](sb-gradle-structure.md)一节中，我们已经提到，将每个微服务的RPC客户端放在xx-client子工程中，现在我们再来回顾下lmsia-abc-client的目录结构。\n\n```shell\n├── build.gradle\n└── src\n    ├── main\n    │   ├── java\n    │   │   └── com\n    │   │       └── coder4\n    │   │           └── lmsia\n    │   │               └── abc\n    │   │                   └── client\n    │   │                       ├── configuration\n    │   │                       │   └── LmsiaAbcThriftClientConfiguration.java\n    │   │                       ├── LmsiaAbcEasyThriftClientBuilder.java\n    │   │                       └── LmsiaK8ServiceThriftClientBuilder.java\n    │   └── resources\n    │       └── META-INF\n    │           └── spring.factories\n    └── test\n\n```\n\n我们简单介绍一下：\n1. LmsiaAbcThriftClientConfiguration: 客户端自动配置，当激活时，自动生成lmsia-abc对应的RPC服务的客户端。引用者直接@Autowired一下，就可以使用了。\n1. LmsiaAbcEasyThriftClientBuilder: EasyThriftClient构造器，主要是自动配置需要。\n1. spring.factories: 与服务端的自动配置类似，需要在这个文件中指定自动配置的类路径，才能让Spring Boot自动扫描到自动配置。\n1. 其他K8ServiceThriftClient相关的部分，我们将在下一小节进行介绍。\n\nLmsiaAbcEasyThriftClientBuilder文件：\n\n```java\npackage com.coder4.lmsia.abc.client;\n\nimport com.coder4.lmsia.abc.thrift.LmsiaAbcThrift;\nimport com.coder4.lmsia.abc.thrift.LmsiaAbcThrift.Client;\nimport com.coder4.lmsia.thrift.client.ThriftClient;\nimport com.coder4.lmsia.thrift.client.builder.EasyThriftClientBuilder;\n\n/**\n * @author coder4\n */\npublic class LmsiaAbcEasyThriftClientBuilder extends EasyThriftClientBuilder<Client> {\n\n    public LmsiaAbcEasyThriftClientBuilder(String host, int port) {\n        setThriftClass(LmsiaAbcThrift.class);\n\n        setHost(host);\n        setPort(port);\n    }\n\n    public static ThriftClient<Client> buildClient(String host, int port) {\n        return new LmsiaAbcEasyThriftClientBuilder(host, port).build();\n    }\n\n}\n\n```\n\n上述Builder完成了实际的参数填充，主要有：\n1. ThriftClient的桩代码类设置(LmsiaAbcThrift.class)\n1. 设置主机名和端口\n\nLmsiaAbcClientConfiguration文件：\n\n```java\npackage com.coder4.lmsia.abc.client.configuration;\n\nimport com.coder4.lmsia.abc.client.LmsiaAbcEasyThriftClientBuilder;\nimport com.coder4.lmsia.abc.client.LmsiaK8ServiceClientBuilder;\nimport com.coder4.lmsia.abc.thrift.LmsiaAbcThrift;\nimport com.coder4.lmsia.abc.thrift.LmsiaAbcThrift.Client;\nimport com.coder4.lmsia.thrift.client.K8ServiceKey;\nimport com.coder4.lmsia.thrift.client.ThriftClient;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.context.annotation.Profile;\n\n@Configuration\npublic class LmsiaAbcThriftClientConfiguration {\n\n    private Logger LOG = LoggerFactory.getLogger(getClass());\n\n    @Bean(name = \"lmsiaAbcThriftClient\")\n    @ConditionalOnMissingBean(name = \"lmsiaAbcThriftClient\")\n    @ConditionalOnProperty(name = {\"lmsiaAbcThriftServer.host\", \"lmsiaAbcThriftServer.port\"})\n    public ThriftClient<Client> easyThriftClient(\n            @Value(\"${lmsiaAbcThriftServer.host}\") String host,\n            @Value(\"${lmsiaAbcThriftServer.port}\") int port\n    ) {\n        LOG.info(\"######## LmsiaAbcClientConfiguration ########\");\n        LOG.info(\"easyClient host = {}, port = {}\", host, port);\n        return LmsiaAbcEasyThriftClientBuilder.buildClient(host, port);\n    }\n\n}\n\n```\n\n如上所示，满足两个条件时，会自动构造LmsiaAbcEasyThriftClient：\n1. 还没有生成其他的LmsiaAbcEasyThriftClient(ConditionalOnMissingBean)\n2. 配置中指定了lmsiaAbcThriftServer.host和lmsiaAbcThriftServer.port\n\n根据我们前面的介绍，大家应该能理解，虽然有自动配置，但上述配置是一种很糟糕的方式。试想一下，如果我们的服务依赖了5个其他RPC服务，那么岂不是要分别配置5组IP和端口？此外，这种方式也无法支持节点的负载均衡。\n\n如何解决这个问题呢？我们将在K8ServiceThriftClient中解决。\n\n本小节的最后，我们看一下spring.factories:\n```shell\norg.springframework.boot.autoconfigure.EnableAutoConfiguration=com.coder4.lmsia.abc.client.configuration.LmsiaAbcThriftClientConfiguration\n\n```\n\n和之前lmsia-abc-server子工程中的文件类似，这里设置了自动配置的详细类路径，方便Spring Boot的自动扫描。\n\n## K8ServiceThriftClient \n\n在对EasyThriftClient的介绍中，我们发现了一个问题，需要单独配置IP和端口，不支持服务自动发现。\n\n此外，在这个客户端的实现中，默认每次都要建立新的连接。而对于后端服务而言，RPC的服务端和客户端多数都是在内网环境中，连接情况比较稳定，可以通过连接池的方式减少连接握手开销，从而提升RPC服务的性能。如果你对连接池的原理还不太熟悉，可以参考[百科连接池](https://baike.baidu.com/item/%E8%BF%9E%E6%8E%A5%E6%B1%A0)\n\n为此，我们本将介绍K8ServiceThriftClient，它很好的解决了上述问题。\n\n首先，我们使用commons-pool2来构建了TTransport层的连接池。\n\nTTransportPoolFactory:\n\n```java\npackage com.coder4.lmsia.thrift.client.pool;\n\nimport com.coder4.lmsia.thrift.client.K8ServiceKey;\nimport org.apache.commons.pool2.BaseKeyedPooledObjectFactory;\nimport org.apache.commons.pool2.PooledObject;\nimport org.apache.commons.pool2.impl.DefaultPooledObject;\nimport org.apache.thrift.transport.TFramedTransport;\nimport org.apache.thrift.transport.TSocket;\nimport org.apache.thrift.transport.TTransport;\n\n/**\n * @author coder4\n */\npublic class TTransportPoolFactory extends BaseKeyedPooledObjectFactory<K8ServiceKey, TTransport> {\n\n    protected static final int THRIFT_CLIENT_DEFAULT_TIMEOUT = 5000;\n\n    protected static final int THRIFT_CLIENT_DEFAULT_MAX_FRAME_SIZE = 1024 * 1024 * 16;\n\n    @Override\n    public TTransport create(K8ServiceKey key) throws Exception {\n        if (key != null) {\n            String host = key.getK8ServiceHost();\n            int port = key.getK8ServicePort();\n            TSocket socket = new TSocket(host, port, THRIFT_CLIENT_DEFAULT_TIMEOUT);\n\n            TTransport transport = new TFramedTransport(\n                    socket, THRIFT_CLIENT_DEFAULT_MAX_FRAME_SIZE);\n\n            transport.open();\n\n            return transport;\n        } else {\n            return null;\n        }\n    }\n\n    @Override\n    public PooledObject<TTransport> wrap(TTransport transport) {\n        return new DefaultPooledObject<>(transport);\n    }\n\n    @Override\n    public void destroyObject(K8ServiceKey key, PooledObject<TTransport> obj) throws Exception {\n        obj.getObject().close();\n    }\n\n    @Override\n    public boolean validateObject(K8ServiceKey key, PooledObject<TTransport> obj) {\n        return obj.getObject().isOpen();\n    }\n\n}\n```\n\n上述代码主要完成以下功能：\n1. 连接超时配置(5秒)\n1. create, 生成新连接（TTransport），这里与之前的EasyThriftClient非常类似，不再赘述\n1. 验证连接是否有效，通过TTransport的isOpen判断。\n\nTTransportPool:\n```java\npackage com.coder4.lmsia.thrift.client.pool;\n\nimport com.coder4.lmsia.thrift.client.K8ServiceKey;\nimport org.apache.commons.pool2.impl.GenericKeyedObjectPool;\nimport org.apache.thrift.transport.TTransport;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * @author coder4\n */\npublic class TTransportPool extends GenericKeyedObjectPool<K8ServiceKey, TTransport> {\n\n    private Logger LOG = LoggerFactory.getLogger(getClass());\n\n    private static int MAX_CONN = 1024;\n    private static int MIN_IDLE_CONN = 8;\n    private static int MAX_IDLE_CONN = 32;\n\n    public TTransportPool(TTransportPoolFactory factory) {\n        super(factory);\n\n        setTimeBetweenEvictionRunsMillis(45 * 1000);\n        setNumTestsPerEvictionRun(5);\n        setMaxWaitMillis(30 * 1000);\n\n        setMaxTotal(MAX_CONN);\n        setMaxTotalPerKey(MAX_CONN);\n        setMinIdlePerKey(MIN_IDLE_CONN);\n        setMaxTotalPerKey(MAX_IDLE_CONN);\n\n        setTestOnCreate(true);\n        setTestOnBorrow(true);\n        setTestWhileIdle(true);\n    }\n\n    @Override\n    public TTransportPoolFactory getFactory() {\n        return (TTransportPoolFactory) super.getFactory();\n    }\n\n    public void returnBrokenObject(K8ServiceKey key, TTransport transport) {\n        try {\n            invalidateObject(key, transport);\n        } catch (Exception e) {\n            LOG.warn(\"return broken key \" + key);\n            e.printStackTrace();\n        }\n    }\n}\n```\n\n上述代码主要是完成连接池的配置，比较直观：\n1. 设置最大连接数1024\n1. 设置最大空闲数32，最小空闲数8，每间隔45秒尝试更改维护连接池中的连接数量。\n1. 当每次\"创建\"、从池子中\"借用\"、\"空闲\"时，检查连接是否有效。\n\n下面我们来看一下如何在K8ServiceThriftClient中使用：\n\n```java\npackage com.coder4.lmsia.thrift.client;\n\nimport com.coder4.lmsia.thrift.client.func.ThriftCallFunc;\nimport com.coder4.lmsia.thrift.client.func.ThriftExecFunc;\nimport com.coder4.lmsia.thrift.client.pool.TTransportPool;\nimport com.coder4.lmsia.thrift.client.pool.TTransportPoolFactory;\nimport org.apache.thrift.TServiceClient;\nimport org.apache.thrift.transport.TTransport;\n\npublic class K8ServiceThriftClient<TCLIENT extends TServiceClient>\n        extends AbstractThriftClient<TCLIENT> {\n\n    private K8ServiceKey k8ServiceKey;\n\n    private TTransportPool connPool;\n\n    @Override\n    public void init() {\n        super.init();\n        // check\n        if (k8ServiceKey == null) {\n            throw new RuntimeException(\"invalid k8ServiceName or k8Serviceport\");\n        }\n        // init pool\n        connPool = new TTransportPool(new TTransportPoolFactory());\n    }\n\n    @Override\n    public <TRET> TRET call(ThriftCallFunc<TCLIENT, TRET> tcall) {\n\n        // Step 1: get TTransport\n        TTransport tpt = null;\n        K8ServiceKey key = getConnBorrowKey();\n        try {\n            tpt = connPool.borrowObject(key);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n\n        // Step 2: get client & call\n        try {\n            TCLIENT tcli = createClient(tpt);\n            TRET ret = tcall.call(tcli);\n            returnTransport(key, tpt);\n            return ret;\n        } catch (Exception e) {\n            returnBrokenTransport(key, tpt);\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public void exec(ThriftExecFunc<TCLIENT> texec) {\n        // Step 1: get TTransport\n        TTransport tpt = null;\n        K8ServiceKey key = getConnBorrowKey();\n        try {\n\n            // borrow transport\n            tpt = connPool.borrowObject(key);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n\n        // Step 2: get client & exec\n        try {\n            TCLIENT tcli = createClient(tpt);\n            texec.exec(tcli);\n            returnTransport(key, tpt);\n        } catch (Exception e) {\n            returnBrokenTransport(key, tpt);\n            throw new RuntimeException(e);\n        }\n    }\n\n    private K8ServiceKey getConnBorrowKey() {\n        return k8ServiceKey;\n    }\n\n    private void returnTransport(K8ServiceKey key, TTransport transport) {\n        connPool.returnObject(key, transport);\n    }\n\n    private void returnBrokenTransport(K8ServiceKey key, TTransport transport) {\n        connPool.returnBrokenObject(key, transport);\n    }\n\n    public K8ServiceKey getK8ServiceKey() {\n        return k8ServiceKey;\n    }\n\n    public void setK8ServiceKey(K8ServiceKey k8ServiceKey) {\n        this.k8ServiceKey = k8ServiceKey;\n    }\n\n}\n```\n\n上述大部分代码和EasyThriftClient非常接近，有差异的部分主要是与连接的\"借用\"、\"归还\"相关的：\n1. 在call和exec中，借用连接\n * getConnBorrowKey先构造一个key，包含了主机名和端口。这里的主机名是[微服务的自动发现](./ms-discovery/msd.md)中提到的Kubernetes服务，如果你对相关原理不太熟悉，可以自行回顾对应章节。\n * 从connPool中借用一个连接（TTransport）\n * 剩余发起rpc调用的步骤就和EasyThriftClient相同了，不再赘述。\n1. 当rpc调用结束后\n * 正常结束，调用connPool.returnObject将TTransport归还到连接池中。\n * 非正常结束，调用connPool.returnBrokenTransport，让连接池销毁这个连接，以防后续借用到这个可能出错的TTransport。\n\n类似的，我们也配套了对应的Builder：\n```java\npackage com.coder4.lmsia.thrift.client.builder;\n\nimport com.coder4.lmsia.thrift.client.EasyThriftClient;\nimport org.apache.thrift.TServiceClient;\n\n/**\n * @author coder4\n */\npublic class EasyThriftClientBuilder<TCLIENT extends TServiceClient> {\n\n    private final EasyThriftClient<TCLIENT> client = new EasyThriftClient<>();\n\n    protected EasyThriftClient<TCLIENT> build() {\n        client.init();\n        return client;\n    }\n\n    protected EasyThriftClientBuilder<TCLIENT> setHost(String host) {\n        client.setThriftServerHost(host);\n        return this;\n    }\n\n    protected EasyThriftClientBuilder<TCLIENT> setPort(int port) {\n        client.setThriftServerPort(port);\n        return this;\n    }\n\n    protected EasyThriftClientBuilder<TCLIENT> setThriftClass(Class<?> thriftClass) {\n        client.setThriftClass(thriftClass);\n        return this;\n    }\n}\n\n```\n\n上述Builder主要是设置所需的两个参数，Host和Port，看起来和EasyThriftClient并没有什么不同？\n\n别着急，我们继续看一下lmsia-abc-client中的集成：\n\n```java\npackage com.coder4.lmsia.abc.client;\n\nimport com.coder4.lmsia.abc.thrift.LmsiaAbcThrift;\nimport com.coder4.lmsia.abc.thrift.LmsiaAbcThrift.Client;\nimport com.coder4.lmsia.thrift.client.K8ServiceKey;\nimport com.coder4.lmsia.thrift.client.ThriftClient;\nimport com.coder4.lmsia.thrift.client.builder.K8ServiceThriftClientBuilder;\n\n/**\n * @author coder4\n */\npublic class LmsiaK8ServiceThriftClientBuilder extends K8ServiceThriftClientBuilder<Client> {\n\n    public LmsiaK8ServiceThriftClientBuilder(K8ServiceKey k8ServiceKey) {\n        setThriftClass(LmsiaAbcThrift.class);\n\n        setK8ServiceKey(k8ServiceKey);\n    }\n\n    public static ThriftClient<Client> buildClient(K8ServiceKey k8ServiceKey) {\n        return new LmsiaK8ServiceThriftClientBuilder(k8ServiceKey).build();\n    }\n\n}\n\n```\n\n在集成的时候，我们需要传入一个key，可以手动制定，也可以自动配置\n\n我们看一下完整的自动配置代码，LmsiaAbcThriftClientConfiguration:\n\n```java\npublic class LmsiaAbcThriftClientConfiguration {\n\n    @Bean(name = \"lmsiaAbcThriftClient\")\n    @ConditionalOnMissingBean(name = \"lmsiaAbcThriftClient\")\n    @ConditionalOnProperty(name = {\"lmsiaAbcThriftServer.host\", \"lmsiaAbcThriftServer.port\"})\n    public ThriftClient<Client> easyThriftClient(\n            @Value(\"${lmsiaAbcThriftServer.host}\") String host,\n            @Value(\"${lmsiaAbcThriftServer.port}\") int port\n    ) {\n        LOG.info(\"######## LmsiaAbcThriftClientConfiguration ########\");\n        LOG.info(\"easyThriftClient host = {}, port = {}\", host, port);\n        return LmsiaAbcEasyThriftClientBuilder.buildClient(host, port);\n    }\n\n    @Bean(name = \"lmsiaAbcThriftClient\")\n    @ConditionalOnMissingBean(name = \"lmsiaAbcThriftClient\")\n    public ThriftClient<LmsiaAbcThrift.Client> k8ServiceThriftClient() {\n        LOG.info(\"######## LmsiaAbcThriftClientConfiguration ########\");\n        K8ServiceKey k8ServiceKey = new K8ServiceKey(K8_SERVICE_NAME, K8_SERVICE_PORT);\n        LOG.info(\"k8ServiceThriftClient key:\" + k8ServiceKey);\n        return LmsiaK8ServiceThriftClientBuilder.buildClient(k8ServiceKey);\n    }\n\n    //...\n\n}\n\n```\n\n对比easyThriftClient和k8ServiceThriftClient不难发现，K8ServiceThriftClient的参数，是通过常量直接写死的。也就是我们在[微服务的自动发现与负载均衡](../ms-discovery/msd.md)中提到的，约定好服务的命名规则。\n\n看下常量定义：\n```java\npublic class LmsiaAbcConstant {\n\n    // ......\n\n    public static final String PROJECT_NAME = \"lmsia-abc\";\n\n    public static final String K8_SERVICE_NAME = PROJECT_NAME + \"-server\";\n\n    public static final int K8_SERVICE_PORT = 3000;\n\n    // ......\n}\n```\n\n这样以来，一旦确定了项目名，那么Kubernetes中的服务名字也确定了。因此，k8ServiceThriftClient自动配置会被自动激活，即只要引用了lmsia-abc-client这个包，就会自动配置好一个RPC客户端，是不是非常方便？\n\n我们来看一下具体的使用例子：\n\n```java\nimport com.coder4.lmsia.thrift.client.ThriftClient;\n\npublic class LmsiaAbctProxy {\n\n    @Autowired\n    private ThriftClient<Client> client;\n\n    public String hello() {\n        return client.call(cli -> cli.sayHi());\n    }\n\n```\n\n至此，我们已经完成了在Spring Boo中集成Thrift RPC的服务端、客户端的工作。\n* 服务端，我们通过ThriftServerConfiguration、ThriftProcessorConfiguration自动配置了Thrift RPC服务端。\n* 客户端，通过Kubernetes的服务功能，自动配置了带服务发现功能的Thrift RPC客户端K8ServiceThriftClient。该客户端同时内置了连接池，用于节省连接开销。\n"
  },
  {
    "path": "legacy/toolchain/README.md",
    "content": "# 研发工具链\n\n子曰: \"工欲善其事必先利其器\"。\n\n本书的开篇已经指出，微服务的架构对研发人员提出了更高的要求。\n\n幸运的是，通过不断完善、改进研发工具链，可以为研发人员提供更高效、更便捷的开发环境。\n\n本书反复强调\"微服务\"、\"研发工具链\"、\"运维工具链\"三者是一个整体，如果只重视微服务的开发技术，而不重视工具链的建设，微服务的架构便无从谈起。\n\n本章将对微服务架构下，常见的研发工具进行介绍。\n\n大致又可分为两部分：\n* 研发环境构建: 主要包括内部帐号管理、代码版本管理、Java依赖管理，这些基础研发环境。\n* 高效研发构建: 主要通过小工具、代码模板、开源项目的引入，降低微服务开发难度，提升开发效率。\n\n现在，让我们开始研发工具链的构建之旅吧！\n"
  },
  {
    "path": "legacy/toolchain/bom.md",
    "content": "# BOM 减少版本冲突\n\n在应用了Gradle构建工具，以及Maven仓库来管理版本依赖后，程序的构建、依赖问题已经得到了基本的解决。\n\n但随着项目的不断发展，一个微服务的依赖可能会越来越多，出现版本冲突的问题。\n\n举个版本冲突的例子：项目依赖的A的0.9版本，同时依赖了项目B，项目B又依赖了项目A的1.0版本。此时，项目会选择A的0.9还是1.0版本呢？\n\n事实上，按照Maven的依赖规则，会选用最小的版本0.9。如果0.9和1.0是API兼容的，那么问题不大。如果1.0的API发生了\"break change\"，那么很遗憾，项目B中的代码会包错，更离谱的是，只有运行时才会发生问题。这类问题经常难以诊断，因此，我们应当尽量减少版本冲突的问题。\n\nBOM(Bill Of Materials)就是为了解决这个问题而生的，它定义了一组依赖管理的项目并约定了对应的版本。其他项目可以直接引用BOM而不用设定对应版本，BOM会自动把缺失的版本补全。\n\n在本书的微服务架构下，我们强烈建议定义公共库的BOM，以减少版本冲突的问题。\n\n应用BOM需要两个步骤:\n1. 新建一个BOM的Maven项目\n1. 在项目中引用该BOM项目\n\n新建一个BOM项目非常简单，只需要一个xml文件\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>com.coder4.lmsia</groupId>\n    <artifactId>pom-parent</artifactId>\n    <version>0.0.4</version>\n    <packaging>pom</packaging>\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <java.version>1.8</java.version>\n    </properties>\n\n    <dependencyManagement>\n        <dependencies>\n            <dependency>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-dependencies</artifactId>\n                <version>1.5.7.RELEASE</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n\t    <dependency>\n\t        <groupId>com.google.guava</groupId>\n\t        <artifactId>guava</artifactId>\n\t        <version>23.0</version>\n\t    </dependency>\n            <!-- lmsia start -->\n\t    <dependency>\n\t        <groupId>com.coder4.lmsia</groupId>\n\t        <artifactId>redis</artifactId>\n\t        <version>0.0.4</version>\n\t    </dependency>\n\t    <dependency>\n\t        <groupId>com.coder4.lmsia</groupId>\n\t        <artifactId>cache</artifactId>\n\t        <version>0.0.5</version>\n\t    </dependency>\n\t    <dependency>\n\t        <groupId>com.coder4.lmsia</groupId>\n\t        <artifactId>rabbitmq</artifactId>\n\t        <version>0.0.2</version>\n\t    </dependency>\n\t    <dependency>\n\t        <groupId>com.coder4.lmsia</groupId>\n\t        <artifactId>thrift-server</artifactId>\n\t        <version>0.0.5</version>\n\t    </dependency>\n\t    <dependency>\n\t        <groupId>com.coder4.lmsia</groupId>\n\t        <artifactId>database</artifactId>\n\t        <version>0.0.1</version>\n\t    </dependency>\n\t    <dependency>\n\t        <groupId>com.h2database</groupId>\n\t        <artifactId>h2</artifactId>\n\t        <version>1.4.196</version>\n\t    </dependency>\n            <!-- lmsia end -->\n        </dependencies>\n    </dependencyManagement>\n\n    <distributionManagement>\n        <repository>\n            <id>nexus_coder4</id>\n            <url>http://192.168.99.100:8081/nexus/content/repositories/releases/</url>\n        </repository>\n        <snapshotRepository>\n            <id>nexus_coder4</id>\n            <url>http://192.168.99.100:8081/nexus/content/repositories/snapshots/</url>\n        </snapshotRepository>\n    </distributionManagement>\n</project>\n```\n\n解释下上面的代码：\n1. 这是一个pom，应用了若干包，并指定了他们的版本\n1. 底部指定了maven仓库的发布地址(如果你有多个不同的maven repo权限才需要设定)\n\n然后看一下在gradle项目中如何引用bom:\n\nbuild.gradle:\n```\nplugins {\n    id \"io.spring.dependency-management\" version \"1.0.3.RELEASE\"\n}\n\napply plugin: 'java'\napply plugin: 'application'\n\n\nrepositories {\n    maven {\n        credentials {\n            username \"$mavenUser\"\n            password \"$mavenPass\"\n        }\n        url 'http://maven.coder4.com/nexus/content/groups/public'\n    }\n}\n\n// import bom\ndependencyManagement {\n    imports {\n        mavenBom 'com.coder4.sbmvt:pom-parent:0.0.1'\n    }\n}\n\ndependencies {\n    // use bom version\n    compile 'org.springframework.boot:spring-boot-starter-web'\n    compile 'com.coder4.lmsia:redis'\n    // Use JUnit test framework\n    testCompile 'junit:junit:4.12'\n}\n\n// Define the main class for the application\nmainClassName = 'App'\n\n```\n\n如上所示，我们通过dependencyManagement插件引入了bom项目，而指定项目时只有group和project、没有版本，版本会自动使用bom中统一定义的。\n\n对于微服务架构，我们可以将使用的数据库、RPC、消息队列、工具类等共用库的版本都放入BOM，以统一依赖的版本。\n"
  },
  {
    "path": "legacy/toolchain/gerrit.md",
    "content": "# gerrit 代码的版本管理与审查\n\n## 为什么选用git作为版本管理系统\n\n在实际工作中，绝大多数的项目都使用了代码的版本管理系统。在应用版本管理系统后，可以代码许多好处，相信大家有有所体会：\n* 团队合作: 应用版本管理系统后，每个团队成员都可以对每个文件进行修改，而不用担心出现不一致、改动丢失、甚至冲突的情况，版本管理系统会负责这些事情。\n* 改动可见: 项目开发往往不是一蹴而就，而是划分为许多个小步骤。我们可以将每个改动作为一次提交，版本管理系统可以展示出两个提交之间的差异，项目的开发进展一目了然。\n* 轻松回滚: 如果我们不小心搞出了一个bug，或者某个设计思路出现了较大错误，可以轻松的回滚到某个之前的版本，这也是版本管理系统为我们提供的便利功能。\n\n在版本系统的选型上，我们选用了git，相比于svn，它具有诸多优点：\n* 分布式、协作方便: git的设计就是分布式版本管理系统，更适用于多人协作。而svn设计理念就是中央式管理，中规中矩但不利于团队协作。\n* 速度更快: 在文件模式上，git基于\"指针式\"设计，比svn更快。在微服务架构下，创建新服务新项目更加频繁，git的速度优势会更加明显。\n* 分支切换: git的分支设计非常轻量级，完全可以在本地完成，而svn则需要完全拉取分支的所有文件，如果你使用svn管理过多分支的大项目，一定对此深有感触。\n* 操作更丰富：git提供了丰富的操作手段，当你使用熟练后，会比使用svn的效率更高。\n\n当然，git也有一个最大的缺点：学习曲线较为陡峭。对于新手而言，svn简单看看文档就能上手，git可能需要几天才能掌握基本操作。\n\n但是，面对git带给我们的种种好处，还是值得仔细学习一下的，篇幅所限，我们不会讨论git的用法。\n\n如果你想仔细学习，推荐阅读[廖雪锋的Git教程](https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000)。\n\n## 为什么代码需要代码审查\n\n如果是一个人做的开源项目，有版本管理系统就足够了。\n\n但对于团队开发，除了版本管理外，一般还应有代码审查(code review)。代码审查的优势如下：\n* 相互检查、提升质量: 在开发过程中，我们自己写出的bug，往往是看不出来的，换个人却很容易发现，就是所谓的\"当局者迷，旁观者清\"。通过相互检查代码，可以有效提升软件质量。\n* 让新成员快速提高: 我们希望新加入的团队成员，可以快速学习、快速成长。阅读项目固然是一个很好的方式，但一个项目往往太大，难以下手，代码审查的粒度是一次提交，更小、更适合新手学习。\n* 边开发边讨论: 在方案设计阶段，我们可能有了大致的方案，但在开发过程中，往往会暴露出更多的问题。代码审查为这些问题的讨论提供了一个合适的契机，大家可以在代码审核的同时进行讨论。\n\n在系统选型方面，我们选用了较为成熟的gerrit作为代码审查系统。\n\n需要指出的是gerrit同时内置了git服务器的功能，因此我们使用gerrit同时作为版本管理和代码审查系统。\n\n## gerrit系统的基本配置\n\n与之前的LDAP类似，我们也将gerrit部署在Kubernetes上。\n\n首先保证物理机上Volume挂载点的创建\n```shell\nminikube ssh\n\n$sudo mkdir /data/gerrit\n$sudo chown -R 999:999 /data/gerrit/\n```\n\n接着我们看一下deployment文件。\ngerrit-deployment.yaml\n```shell\npiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: gerrit-deployment\nspec:\n  selector:\n    matchLabels:\n      app: gerrit\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        app: gerrit\n    spec:\n      restartPolicy: Always \n      nodeSelector:\n        kubernetes.io/hostname: minikube \n      containers:\n      - name: gerrit-ct\n        image: openfrontier/gerrit:2.15.1\n        ports:\n        - containerPort: 8080\n          hostPort: 80 \n        - containerPort: 29418\n          hostPort: 29418\n        volumeMounts:\n        - mountPath: \"/var/gerrit/review_site\"\n          name: volume \n        env:\n        - name: GITWEB_TYPE\n          value: gitiles\n        - name: AUTH_TYPE \n          value: LDAP\n        - name:  LDAP_SERVER \n          value: ldap://192.168.99.100\n        - name: LDAP_ACCOUNTBASE \n          value: \"dc=coder4,dc=com\"\n        - name: LDAP_ACCOUNTPATTERN\n          value: \"(cn=${username})\"\n        - name: LDAP_ACCOUNTSSHUSERNAME\n          value: \"${cn}\"\n        - name: LDAP_ACCOUNTFULLNAME\n          value: \"${sn}\"\n        - name: LDAP_USERNAME\n          value: \"cn=guest,dc=coder4,dc=com\"\n        - name: LDAP_PASSWORD\n          value: \"guest123\"\n        - name: WEBURL \n          value: \"http://192.168.99.100\"\n      volumes:\n      - name: volume \n        hostPath:\n          path: /data/gerrit/\n```\n\n虽然文件很长，但并不复杂，我们简单解读下：\n* Docker镜像为openfrontier/gerrit:2.15.1\n* 端口映射8080到物理机的的80端口上\n* 挂载点/var/gerrit/review_site \n* 使用LDAP作为帐号接入，具体配置在之前LDAP一节已经见识过了，这里不再赘述。\n* WEB跳转URL定义为 http://物理机IP\n\n下面启动一下：\n```shell\nkubectl apply -f ./gerrit-deployment.yaml\n```\n\n启动成功后，我们访问gerrit，然后点击右上角的\"Sign In\"即可登录。这里的帐号，填写之前创建的一个LDAP内部帐号。需要特别说明的是，第一个登录的用户，会被gerrit认为是超级管理员，所以请慎重选择。\n\n![gerrit登录成功](./gerrit-login-succ.png \"gerrit登录成功j\")\n\n如果一切顺利的话，就会登录成功了。至此，我们已经完成了gerrit服务器的基本配置。\n\n## gerrit常用插件\n\ngerrit系统的基本功能比较简单，需要配合插件才能发挥出更大优势\n\n在此，我们先安装两个系统内置的插件：\n* commit msg长度检查\n* 项目下载url生成器\n\n安装插件是通过ssh命令完成的，一次，首先要将ssh密钥的公钥上传到gerrit上。\n\n如果你还没有ssh密钥，可以使用sshkeygen生成，这里不做详细展开。\n\n点击右上角的姓名 -> Settings -> SSH Public Keys，粘贴后点击\"Add\"。\n\n然后添加插件:\n```shell\nssh -p 29418 lihy@192.168.99.100 gerrit plugin install 'jar:file:/var/gerrit/review_site/bin/gerrit.war!/WEB-INF/plugins/download-commands.jar'\nssh -p 29418 lihy@192.168.99.100 gerrit plugin install 'jar:file:/var/gerrit/review_site/bin/gerrit.war!/WEB-INF/plugins/commit-message-length-validator.jar'\n```\n\n## gerrit项目的权限控制\n\ngerrit默认的权限配置是对所有人(包括注册用户和匿名用户)开放所有项目。\n\n这样的设置可能过为宽松，可以自行更改。\n\n使用管理员帐号登录，然后进入Projects -> All Projects，点击底部的顶部的\"Access\"，点击Edit。然后找到 Reference: refs/* -> Read，修改为 -> Block Anonymous Users，修改完成后点击\"Save for change\"。\n\n![修改匿名用户读权限](./gerrit-block-anonymous-users.png \"修改匿名用户读权限\")\n\n我们可以登出当前用户，再次访问gerrit主页，可以发现，在未登录状态，无法找到任何review和项目了。\n\n## 第一个gerrit代码review\n\n下面我们尝试用gerrit完成一个完整的流程:从新建项目、提交、到审核代码。\n\n我们尝试新建一个项目：Projects -> Create New Project:\n* 项目名为lmsia-xyz\n* 继承自All-Projects\n然后点击\"Create Project\"\n\n创建完成后，我们就可以将代码克隆到本地进行开发了。\n\n选择：Projects -> List 找到lmsia-xyz并点击，在顶部，可以找到Clone工具栏，选择右侧的ssh，底下会出现一行命令：\n```shell\ngit clone ssh://lihy@192.168.99.100:29418/lmsia-xyz\n```\n\n我们在本地执行这行命令，即可成功得克隆代码\n```shell\ngit clone ssh://lihy@192.168.99.100:29418/lmsia-xyz\nCloning into 'lmsia-xyz'...\nremote: Counting objects: 2, done\nremote: Finding sources: 100% (2/2)\nremote: Total 2 (delta 0), reused 0 (delta 0)\nReceiving objects: 100% (2/2), done.\nChecking connectivity... done.\n\n```\n\n如果报权限错误，一般是ssh密钥配置的不对，请检查gerrit个人资料中的key是否为本地设置的公钥。\n\n配置修改后，可以自行通过这条命令测试\n```shell\nssh -p 29418 lihy@192.168.99.100\n\n  ****    Welcome to Gerrit Code Review    ****\n\n  Hi 李赫元, you have successfully connected over SSH.\n\n  Unfortunately, interactive shells are disabled.\n  To clone a hosted Git repository, use:\n\n  git clone ssh://lihy@192.168.99.100:29418/REPOSITORY_NAME.git\n\nConnection to 192.168.99.100 closed.\n\n```\n\n下面我们新建一个文件：\n```shell\ntouch README.md\n```\n\n添加并提交：\n```shell\ngit add .\n\ngit commit -m \"ADD: README.md\"\n```\n\n至此，我们已经完成了代码的提交，当然这只是提交到本地git仓库中。\n\n我们还需要推送到gerrit仓库中供别人审核。\n\n在可以推送到gerrit之前，还需要进行2个配置:\n1. (每台机器配置一次)若你的操作系统用户名和gerrit用户名一致，需要配置ssh选项。\n1. (每个项目配置一次)配置项目的gerrit远程仓库\n1. (每个项目配置一次)配置项目推送到gerrit后默认的代码审核人\n\n首先是ssh配置，以我的环境为例，我的操作系统用户名是coder4，而gerrit用户名是lihy，于是在~/.ssh/config中添加如下配置：\n```shell\nHost 192.168.99.100\n    User lihy\n    IdentityFile ~/.ssh/id_rsa\n    Hostname 192.168.99.100\n    Port 29418\n```\n这个配置并不复杂，就是告诉操作系统，当连接192.168.99.100这个host时，默认用户改为lihy而不是系统默认的coder4\n\n而上面每个项目需要执行一次的2和3稍微，这个操作稍微复杂一些，所以我将它合并成了一个脚本，方便大家调用。\n```shell\n#!/usr/bin/env bash\n\nGERRIT_HOST=\"192.168.99.100\"\nEMAIL_POSTFIX=\"coder4.com\"\n\nset -e\n\nfunction join { local IFS=\"$1\"; shift; echo \"$*\"; }\n\nif [ -z \"$1\" ]; then\n    echo \"Usage: $0 reviewer[,reviewer ...]\"\n    exit 1\nfi\n\nset -u\n\nif [ -z `git remote | grep origin` ]; then\n    echo \"Remote origin not found, please clone this repository correctly or add origin remote by 'git remote add'.\"\n    exit 1\nfi\n\nscp -p -P 29418 $GERRIT_HOST:hooks/commit-msg .git/hooks/\n\ncat > .git/hooks/pre-commit << EOF\n##!/bin/sh\nif git-rev-parse --verify HEAD >/dev/null 2>&1 ; then\n   against=HEAD\nelse\n   # Initial commit: diff against an empty tree object\n   against=4b825dc642cb6eb9a060e54bf8d69288fbee4904\nfi\n# Find files with trailing whitespace\nfor FILE in \\`exec git diff-index --check --cached \\$against -- | sed '/^[+-]/d' | sed -E 's/:[0-9]+:.*//' | uniq\\` ; do\n    # Fix them!\n    sed -i '' -E 's/[[:space:]]*$//' \"\\$FILE\"\n    git add \"\\$FILE\"\ndone\nEOF\nchmod a+x .git/hooks/pre-commit\n\noriginURL=`git remote -v | grep fetch | perl -nle'print $& if m{(?<=origin\\t)\\S*}'`\n\n(git remote remove review >& /dev/null || exit 0)\n\ngit remote add review $originURL\n\nIFS=',' read -a reviewers <<< \"$1\"\n\nsed -i '/\\+refs\\/heads\\/\\*:refs\\/remotes\\/review\\/\\*/d' .git/config\nfor i in \"${!reviewers[@]}\"; do\n  reviewers[$i]=\"--reviewer=${reviewers[$i]}@$EMAIL_POSTFIX\"\ndone\necho -e \"\\tpush = HEAD:refs/for/master\" >> .git/config\necho -e \"\\treceivepack = git receive-pack `join \" \" ${reviewers[@]}`\" >> .git/config\n\n```\n如上的脚本做了3件事情:\n* 从gerrit上下载commit-msg的钩子，这是gerrit生成Change-ID所必须的。\n* 配置远程review仓库\n* 配置推送后默认的代码审核人\n\n执行一下，默认自己和张三审核：\n```shell\ninitGerrit.sh lihy,zhangsan\n```\n\n上述配置完成后，就可以推送你的第一个code review了：\n```shell\ngit push review\n\nCounting objects: 3, done.\nWriting objects: 100% (3/3), 244 bytes | 0 bytes/s, done.\nTotal 3 (delta 0), reused 0 (delta 0)\nremote: Processing changes: refs: 1, done    \nremote: ERROR: [127a929] missing Change-Id in commit message footer\nremote: \nremote: Hint: To automatically insert Change-Id, install the hook:\nremote:   gitdir=$(git rev-parse --git-dir); scp -p -P 29418 lihy@192.168.99.100:hooks/commit-msg ${gitdir}/hooks/\nremote: And then amend the commit:\nremote:   git commit --amend\nremote: \nTo ssh://lihy@192.168.99.100:29418/lmsia-xyz\n ! [remote rejected] HEAD -> refs/for/master ([127a929] missing Change-Id in commit message footer)\nerror: failed to push some refs to 'ssh://lihy@192.168.99.100:29418/lmsia-xyz'\n\n```\n\n然而我们发现还是执行失败，这是因为，我们先执行了commit后执行了initGerrit，导致commit时候没有Change-ID。\n\n我们可以按照提示补救一下：\n```shell\ngit commit --amend\n```\n\n再次执行推送，成功：\n```shell\ngit push review\nCounting objects: 3, done.\nWriting objects: 100% (3/3), 285 bytes | 0 bytes/s, done.\nTotal 3 (delta 0), reused 0 (delta 0)\nremote: Processing changes: new: 1, done    \nremote: \nremote: New Changes:\nremote:   http://192.168.99.100/#/c/lmsia-xyz/+/21 ADD: README.md\nremote: \nTo ssh://lihy@192.168.99.100:29418/lmsia-xyz\n * [new branch]      HEAD -> refs/for/master\n\n```\n\n我们到gerrit上看一眼，发现已经有了这个推送：\n\n![第一个review](./gerrit-first-review.png \"第一个review\")\n\n我们点击进去，自行+2，然后点击Submit，如下两图所示。\n\n![+2](./gerrit-p2.png \"代码审核+2\")\n![代码合并](./gerrit-submit.png \"代码合并\")\n\n此时，代码就被成功合并进master分支了。\n\n我们的gerrit默认配置了gitweb,即可以通过网页的方式查看项目的完整源码: Plugsin -> gitiles，界面如下图所示：\n\n![代码浏览](./gerrit-gittiles.png \"代码浏览\")\n\n通过选择不同项目，可以查看不同分支的完整代码。\n\n至此，我们完成了Gerrit服务的搭建，并通过完整的例子演示了项目的创建、克隆、开发、提交、审核流程。\n\nGerrit还有很多强大的功能，例如Web上可以创建分支、Rebase代码等等，如果你想探索这些高级用法，可以参考[官方文档](https://gerrit-documentation.storage.googleapis.com/Documentation/2.15.2/index.html)。\n"
  },
  {
    "path": "legacy/toolchain/kanboard.md",
    "content": "# Kanboard Scrum看板\n\n"
  },
  {
    "path": "legacy/toolchain/ldap.md",
    "content": "# LDAP 内部账号管理系统\n\n## LDAP及其必要性 \n\n对于任何一个研发团队，一套内部通用的帐号管理系统都是必不可少的。请注意我的用词:\"内部通用\"。\n\n公司内部可能有各种系统：\n* 行政层面的OA系统、邮件系统、会议室预订系统。\n* 研发团队内部又可能有代码管理、项目进度管理、Bug追踪、依赖管理、Wiki等等。\n\n如果没有内部通用帐号，那么每来一个新员工，就需要到上述所有系统中，分别注册一次。想象一下，这是多么让人头疼的事情！\n\n因此，我们建议团队一定要拥有一套\"内部通用\"的帐号管理系统。\n\n在这里，我们选用了LDAP(Lightweight Directory Access Protocol)。是一个开放的，中立的，工业标准的应用协议，通过IP协议提供访问控制和维护分布式信息的目录信息。\n\n在技术型团队中，LDAP可以当作内部帐号管理系统来使用。此外，LDAP可以很轻松地与其他系统对接，我们后面即将构建的代码管理、版本管理，都将通过LDAP帐号接入。\n\n\n## OpenLDAP服务的初步配置\n\n能提供LDAP服务的开源项目有很多，我们选用了较为成熟的开源服务器OpenLDAP。\n\n虽然OpenLDAP并不是微服务，但我们依然放到Kubernetes集群部署，主要原因是：\n* 方便运维: 如果不用Docker，就需要手动的安装、配置。一旦物理服务器发生故障，需要迁移服务时，就需要重新执行这些操作。运维起来非常麻烦。\n* 方便备份与恢复: 对于这类帐号系统，可用性倒要求并不高(偶尔挂掉1个小时，能接受)，但是对数据安全性，特别是备份有较高要求。使用Docker后，我们只需要将产生的数据挂载到Volume上，然后定期备份Volume即可。\n\n\n来看一下部署文件openldap-deployment.yaml:\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: openldap-deployment\nspec:\n  selector:\n    matchLabels:\n      app: openldap\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        app: openldap\n    spec:\n      restartPolicy: OnFailure \n      nodeSelector:\n        kubernetes.io/hostname: minikube \n      containers:\n      - name: openldap-ct\n        image: osixia/openldap:1.1.9 \n        ports:\n        - containerPort: 389\n          hostPort: 389 \n        - containerPort: 636\n          hostPort: 636 \n        volumeMounts:\n        - mountPath: \"/etc/ldap/slapd.d\"\n          name: volume \n          subPath: conf\n        - mountPath: \"/var/lib/ldap\"\n          name: volume\n          subPath: data\n        env:\n        - name: LDAP_TLS \n          value: \"false\"\n        - name: LDAP_DOMAIN \n          value: \"coder4.com\"\n        - name: LDAP_ADMIN_PASSWORD \n          value: \"admin123\"\n        - name: LDAP_READONLY_USER \n          value: \"true\"\n        - name: LDAP_READONLY_USER_USERNAME\n          value: \"guest\"\n        - name: LDAP_READONLY_USER_PASSWORD \n          value: \"guest123\"\n      volumes:\n      - name: volume \n        hostPath:\n          path: /data/openldap/\n```\n\n这是一个很长的文件，我们来逐条解释下：\n* restartPolicy: 虽然这是一个内部服务，但我们还是希望它能稳定提供服务。如果万一服务挂掉，希望能自动重启。因此我们设置自动重启策略为OnFailure。\n* nodeSelector: 我们强制选择了主机名。即这个Pod只能启动在minikube这台hostname的主机上，为什么呢？因为我们的OpenLDAP服务使用了本地Volume(hostVolume)，如果不固定机器，允许Pod在任意物理机启动的话，对应Volume并不会自动迁移，导致之前的账户信息\"丢失\"。因此，对于需要使用Volume的服务，要么选择一种可自动迁移的Volume，要么就需要绑定到一台物理机上。如果你想选用自动迁移的Volume，可以参考[官方Volumes文档](https://kubernetes.io/docs/concepts/storage/volumes/)。\n* ports: 我们直接对集群外暴露了389和636两个端口。在实际生产中，我建议选择一台独立的物理机部署所有的内部服务(ldap、maven、git等)。为什么这样搞呢？如果物理机是固定的，我们可以给它分配一个固定的办公网IP，甚至固定的办公网DNS域名，然后简单地通过暴露端口的方式，就可以对全部办公网提供服务了。\n* volumeMounts & volumes: 定义了两个volume挂载点，分别挂载到容器的/etc/ldap/slapd.d(配置)和/var/lib/ldap(数据)目录上。对应的物理机挂载目录在/data/openldap/conf和/data/openldap/data上。\n* env: 通过环境变量完成了一些初始化的设定，具体如下。\n * 不用加密协议[^1]\n * 设置域为coder4.com，可以根据你的需求自行更改。\n * 创建系统管理员帐号，密码是admin123，这是一个超级管理员，对应用户名是admin(无法更改)\n * 创建系统只读帐号，用户名和密码是guest/guest123。这主要是用于其他服务与OpenLDAP服务的通信，只能读取、验证信息，不能做任何更改。\n\n在部署前，我们先要保证物理机上的挂载点存在。\n```shell\nminikube ssh\n$ cd /data\n$ sudo mkdir openldap\n```\n\n然后部署OpenLDAP服务：\n```shell\nkubectl apply -f ./openldap-deployment.yaml\n```\n\n查看下状态，启动成功了：\n```shell\nkubectl get pods\n\nNAME                                           READY     STATUS    RESTARTS   AGE\nopenldap-deployment-7d6b7875f-hxqxf            1/1       Running   0          14m\n\n```\n\n获取集群的IP：\n```shell\nminikube ip\n\n192.168.99.100\n```\n\n验证下，端口已经成功暴露给了集群外：\n```shell\ntelnet 192.168.99.100 389\nTrying 192.168.99.100...\nConnected to 192.168.99.100.\nEscape character is '^]'.\n^]\n\n```\n\n操作ldap集群，需要安装一些工具，以Ubuntu为例：\n```shell\nsudo apt-get install ldap-utils\n```\n\n有了工具后，两个系统帐号已经创建成功：\n```shell\nldapwhoami -h 192.168.99.100 -p 389 -D \"cn=admin,dc=coder4,dc=com\" -w admin123\n\ndn:cn=admin,dc=coder4,dc=com\n\nldapwhoami -h 192.168.99.100 -p 389 -D \"cn=guest,dc=coder4,dc=com\" -w guest123\n\ndn:cn=guest,dc=coder4,dc=com\n```\n\n至此，我们已经完成了OpenLDAP的基础配置，并且成功创建了两个系统帐号。\n\n\n## 创建内部用户\n\n在刚才的配置中，我们创建了两个系统帐号，但在实际工作中，团队成员一般不会使用系统帐号。\n\n对于一个团队成员，它的帐号至少需要有如下属性：\n* 用户名, 一般是纯英文、拼音缩写\n* 中文姓名，这个不解释了\n* 密码，最好不是明文，而是加密存储\n* 邮箱，公司内部的电子邮箱地址\n\n大公司的内部，会细分为多个团队，此时还应当将用户划分到相应的属组。由于篇幅所限，我们在此不讨论属组的问题。\n\n\n在密码加密方面，我们采用ssha，它需要命令slappasswd，你可以在任何安装了openldap的机器上找到它：\n```shell\nslappasswd -h {ssha} -s pass123\n\n{SSHA}yG3DQj7iol10+fzWoeBAgoZ+D+h9uQre\n```\n\n上述即生成了一个ssha加密过的密码pass123。\n\n我们前面已经提到，LDAP是一个\"目录式\"的权限管理服务。其本身规则非常复杂到可以单独写一本书:-)\n\n本书不会对其规则进行过多讲解，这里先提供了一个简单的模板，供大家学习。\n\n./users.ldif\n```shell\nversion: 1\n\n# users org\ndn: ou=users,dc=coder4,dc=com\nobjectClass: top\nobjectClass: organizationalUnit\nou: users\n\n# group org\ndn: ou=groups,dc=coder4,dc=com\nobjectClass: top\nobjectClass: organizationalUnit\nou: groups\n\n# define users here\ndn: cn=lihy,ou=users,dc=coder4,dc=com\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: lihy\nsn:: 5p2O6LWr5YWD\nmail: lihy@coder4.com\nuserPassword: {SSHA}yG3DQj7iol10+fzWoeBAgoZ+D+h9uQre \n\ndn: cn=zhangsan,ou=users,dc=coder4,dc=com\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: zhangsan \nsn:: 5byg5LiJ\nmail: zhangsan@coder4.com\nuserPassword: {SSHA}yG3DQj7iol10+fzWoeBAgoZ+D+h9uQre\n\n# should also modify here if insert new user\ndn: cn=Users,ou=groups,dc=coder4,dc=com\nobjectClass: top\nobjectClass: groupOfUniqueNames\ncn: Users\nuniqueMember: cn=lihy,ou=users,dc=coder4,dc=com\nuniqueMember: cn=zhangsan,ou=users,dc=coder4,dc=com\n\n# define admin here\ndn: cn=Admin,ou=groups,dc=coder4,dc=com\nobjectClass: top\nobjectClass: groupOfUniqueNames\ncn: Admin\nuniqueMember: cn=lihy,ou=users,dc=coder4,dc=com\n\n```\n\n简单解释下：\n* 我们创建了2个组users和groups，前者存放用户，后者表示用户的属组。\n* 定义两个用户lihy和zhangsan，他们的密码用前面提到的SSLA加密\n* 将两个用户加入Users组内\n* 将lihy用户加入管理员组内\n\n\n我们来应用这个模板：\n```shell\nldapadd -c -h 192.168.99.100 -p 389 -w admin123 -D \"cn=admin,dc=coder4,dc=com\" -f ./users.ldif\n```\n\n如上，需要用admin帐号，-c选项是忽略所有错误，继续执行。\n\n验证一下新增的内部用户：\n```shell\nldapwhoami -h 192.168.99.100 -p 389 -D \"cn=lihy,ou=users,dc=coder4,dc=com\" -w pass123\n\ndn:cn=lihy,ou=users,dc=coder4,dc=com\n```\n\n添加新用户，需要操作三个步骤：\n1. 在user.idlf中增加用户的定义\n1. 在user.idlf对应属组中添加\n1. 执行ldapadd命令\n\n## LDAP系统管理脚本\n\n不用我说大家也明白，上述步骤真的是非常繁琐，而且容易出错。\n\n面对这种情况，大家可以选用第三方的工具来管理LDAP帐号，例如phpLDAPadmin，但是这需要额外维护一套系统，不免有些笨重。\n\n为了降低维护成本，我提供了几个简单的小脚本，以满足日常的管理工作。\n\n添加帐号，ldap_add.sh\n```shell\n#!/bin/bash\n\n# const\nLDAP_SERVER_IP=\"192.168.99.100\"\nLDAP_SERVER_PORT=\"389\"\nLDAP_ADMIN_USER=\"cn=admin,dc=coder4,dc=com\"\nLDAP_ADMIN_PASS=\"admin123\"\n\nif [ x\"$#\" != x\"3\" ];then\n    echo \"Usage: $0 <username> <password> <realname>\"\n    exit -1\nfi\n\n# param\nUSERNAME=\"$1\"\nPASSWORD=\"$2\"\nENCRYPT_PASSWORD=$(slappasswd -h {ssha} -s \"$PASSWORD\")\nREALNAME=\"$3\"\nREALNAME_BASE64=$(echo -n $REALNAME | base64)\n\n# add count & group \ncat <<EOF | ldapmodify -c -h $LDAP_SERVER_IP -p $LDAP_SERVER_PORT -w $LDAP_ADMIN_PASS -D $LDAP_ADMIN_USER \ndn: cn=$USERNAME,ou=users,dc=coder4,dc=com\nchangetype: add\nobjectClass: top\nobjectClass: person\nobjectClass: organizationalPerson\nobjectClass: inetOrgPerson\ncn: $USERNAME\nsn:: $REALNAME_BASE64\nmail: $USERNAME@coder4.com\nuserPassword: $ENCRYPT_PASSWORD\n\ndn: cn=Users,ou=groups,dc=coder4,dc=com\nchangetype: modify\nadd: uniqueMember\nuniqueMember: cn=$USERNAME,ou=users,dc=coder4,dc=com\nEOF\n```\n\n上述脚本通过ldapmodify命令，自动完成了我们之前提到的三个步骤。\n\n我们试着添加新用户lisi\n```shell\n./ldap_add.sh lisi pass123 李四\n\nadding new entry \"cn=lisi,ou=users,dc=coder4,dc=com\"\n\nmodifying entry \"cn=Users,ou=groups,dc=coder4,dc=com\"\n```\n\n验证一下，添加成功\n```shell\nldapwhoami -h 192.168.99.100 -p 389 -D \"cn=lisi,ou=users,dc=coder4,dc=com\" -w pass123\n\ndn:cn=lisi,ou=users,dc=coder4,dc=com\n```\n\n第二个常见的情况是，修改密码, ./ldap_modify_password.sh：\n```shell\n#!/bin/bash\n\n# const\nLDAP_SERVER_IP=\"192.168.99.100\"\nLDAP_SERVER_PORT=\"389\"\nLDAP_ADMIN_USER=\"cn=admin,dc=coder4,dc=com\"\nLDAP_ADMIN_PASS=\"admin123\"\n\nif [ x\"$#\" != x\"2\" ];then\n    echo \"Usage: $0 <username> <newPassword>\"\n    exit -1\nfi\n\n# param\nUSERNAME=\"$1\"\nPASSWORD=\"$2\"\nENCRYPT_PASSWORD=$(slappasswd -h {ssha} -s \"$PASSWORD\")\n\n# modify\ncat <<EOF | ldapmodify -c -h $LDAP_SERVER_IP -p $LDAP_SERVER_PORT -w $LDAP_ADMIN_PASS -D $LDAP_ADMIN_USER \ndn: cn=$USERNAME,ou=users,dc=coder4,dc=com\nchangetype: modify\nreplace: userPassword\nuserPassword: $ENCRYPT_PASSWORD\nEOF\n\n```\n\n我们尝试修改lisi的密码:\n```shell\n./ldap_modify_password.sh lisi hahaha\nmodifying entry \"cn=lisi,ou=users,dc=coder4,dc=com\"\n```\n\n验证一下，新密码已经修改成功：\n```shell\nldapwhoami -h 192.168.99.100 -p 389 -D \"cn=lisi,ou=users,dc=coder4,dc=com\" -w hahaha\n\ndn:cn=lisi,ou=users,dc=coder4,dc=com\n```\n\n最后一个场景是删除用户，这里我们只删除用户，不删除其加入的属组\n```shell\n#!/bin/bash\n\n# const\nLDAP_SERVER_IP=\"192.168.99.100\"\nLDAP_SERVER_PORT=\"389\"\nLDAP_ADMIN_USER=\"cn=admin,dc=coder4,dc=com\"\nLDAP_ADMIN_PASS=\"admin123\"\n\nif [ x\"$#\" != x\"1\" ];then\n    echo \"Usage: $0 <username>\"\n    exit -1\nfi\n\n# param\nUSERNAME=\"$1\"\n\n# delete user \nldapdelete -c -h $LDAP_SERVER_IP -p $LDAP_SERVER_PORT -w $LDAP_ADMIN_PASS -D $LDAP_ADMIN_USER \"cn=$USERNAME,ou=users,dc=coder4,dc=com\"\n\n```\n\n尝试删除一下：\n```shell\n./ldap_delete.sh zhangsan\n```\n\n然后验证下，确实无法登录了\n```shell\nldapwhoami -h 192.168.99.100 -p 389 -D \"cn=zhangsan,ou=users,dc=coder4,dc=com\" -w pass123\nldap_bind: Invalid credentials (49)\n```\n\n至此，我们完成了LDAP服务的构建，并可以通过简单的脚本完成帐号的添删改操作。\n\n[^1]: 如果你十分看中帐号服务对外通信的安全性，建议还是开启，具体可以参考[docker-openldap](https://github.com/osixia/docker-openldap))\n"
  },
  {
    "path": "legacy/toolchain/nexus.md",
    "content": "# Nexus 私有maven仓库\n\n依赖管理是技术栈的重要一环，几乎所有的现代编程语言都拥有自己的依赖管理系统。\n\n如果你在很久以前就从事了Java开发，或者参与过一些\"不太正规\"的项目，一定经历有过\"jar包随便拷、jar包满天飞\"的经历。在这种情况下，每次上线发布、升级jar包都是非常痛苦的事情。\n\n大概从2003年开始，构建工具逐渐走入Java开发者的视野，Maven是构建工具中应用最为广泛的工具之一，它在提供构建功能的同时，也自带了强大的依赖管理功能。采用maven后，我们只需要定义xml就可以自动下载依赖的jar包，而不需要\"手动将jar包拷来拷去\"。\n\n近几年来，作为一种更高效的构建工具 - Gradle - 逐渐崛起，在一些开发领域(如Android)，Gradle已经完全取代了Maven成为事实上的构建标准。\n\n尽管从构建工具的角度而言，Maven的地位有所下降，但它在Java依赖管理子领域的地位却不容撼动。Gradle默认也是直接采取Maven的依赖管理框架(只不过换为更简便的描述语言)。\n\n在[架构概览](architecture/README.md)一章中，我们已经说明：在选型上，我们使用Gradle作为构建工具，但依然采用Maven来管理依赖。\n\n在Maven的依赖管理方面，我们将使用Nexus搭建私有Maven服务器。为什么要搭建私服呢？\n\n这和为什么搭建私有Git服务器却不用GitHub公开仓库是一个道理：没有公司愿意将自己的代码暴露给全世界:-)\n\n## Nexus仓库的基本配置\n\n与前两节类似，我们首先在Kubernetes上部署Nexus服务。\n\n创建之前，先在物理机上创建Volume挂载点:\n```shell\nminikube ssh\n\n$sudo mkdir /data/nexus\n$sudo chmod -R 777 /data/nexus/\n```\n\n这里因为nexus需要有一个文件锁，默认权限是不够的，我们给了777权限，如果你觉得过于宽松，可以自行更改Kubernetes的启动用户，并设定相应权限。\n\n看一下部署文件, nexus-deployment.yaml:\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: nexus-deployment\nspec:\n  selector:\n    matchLabels:\n      app: nexus\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        app: nexus\n    spec:\n      restartPolicy: Always \n      nodeSelector:\n        kubernetes.io/hostname: minikube \n      containers:\n      - name: nexus-ct\n        image: sonatype/nexus:2.14.8\n        ports:\n        - containerPort: 8081\n          hostPort: 8081 \n        volumeMounts:\n        - mountPath: \"/sonatype-work\"\n          name: volume \n      volumes:\n      - name: volume \n        hostPath:\n          path: /data/nexus/\n```\n\n部署一下:\n```shell\nkubectl apply -f ./nexus-deployment.yaml\n```\n\n一切顺利的话，稍等一会访问\"http://192.168.99.100:8081/nexus\"，部署成功，如下图所示:\n\n![nexus部署成功](./nexus-welcome.png \"nexus部署成功\")\n\n## Nexus接入LDAP帐号\n\n服务虽然好了，但还没有接入LDAP帐号系统，Nexus中接入LDAP帐号较为繁琐，请耐心操作完。\n\n首先，设置一下LDAP的连接配置。\n\n1. 使用默认管理员帐号登录，用户名admin，密码admin123\n1. 点击左侧菜单\"Security\" -> \"LDAP Configuration\"\n1. 设置LDAP配置如下\n 1. Protocol: ldap\n 1. Hostname: 192.168.99.100\n 1. SearchBase: dc=coder4,dc=com\n 1. Authentication Method: Simple Authentication\n 1. Username: cn=guest,dc=coder4,dc=com\n 1. Password: guest123\n 1. Base DN: ou=users\n 1. Object Class: inetOrgPerson\n 1. User ID Attribute: cn\n 1. Real Name Attribute: sn\n 1. E-Mail Attribute: mail\n 1. Group Element Mapping: 不选中\n\n上述配置稍显繁琐，请耐心完成。都配置完成后，点击\"Save\"。\n\n此外，点击底部的\"Check User Mapping\"，如果一切配置正确，可以展示所有的列表，如下图所示。\n\n![nexus配置ldap帐号接入](./nexus-ldap-config.png \"nexus配置ldap帐号接入\")\n\n第二步，下面我们来更改默认认证方式为LDAP。\n\n点击左侧菜单\"Administration\" -> \"Server\"，进行如下配置:\n1. 在Security Settings中，将右侧的\"OSS LDAP Authentication Realm\"加入到左边，并将其拖动到最顶部。\n1. 取消勾选\"Anoymous Access\"。\n\n![配置认证方式为LDAP](./nexus-auth-config.png \"配置认证方式为LDAP\")\n\n配置可以参考上图，设置好后，点击\"Save\"。\n\n最后，我们需要对所有用户配置权限，注意，每次LDAP新接入用户后，都要执行下述操作。\n\n点击左侧菜单\"Security\" -> \"Users\" ，执行下述操作:\n1. 点击\"All Configured Users\"旁边的小箭头，选择\"LDAP\"\n1. 点击\"Refresh\"，此时就能拿到所有LDAP中的用户了。\n1. 选中一个要操作的用户，例如\"lihy\"，选择底部的\"Config\"，然后\"Role Management\"。\n1. 一般用户给\"Nexus Deployment Role\"就可以了，管理员可以给\"Nexus Administrator Role\"。\n1. 设置好后点击\"Save\"\n\n![nexus给用户权限](./nexus-role.png \"nexus给用户权限\")\n\n一个配置好的结果如上图所示。\n\n至此，我们已经成功接入了LDAP，试着用配置好的用户登录下，发现可以登录成功。\n\n## 配置Nexus中央仓库的缓存\n\nMaven依赖仓库也是分布式，我们最长用的，是\"Maven Central\"这个中央仓库。\n\n我们建议将中央仓库的索引缓存到Nexus私服上，这大约需要20GB的空间。\n\n使用管理员帐号登录后，点击左侧菜单\"View/Repositories\":\n1. 选择\"Repositories\"\n1. 在右侧选择\"Central\"这个仓库\n1. 底部\"Configuration\"配置\n 1. Remote Storage Location: http://maven.aliyun.com/nexus/content/repositories/central/ (这里我们使用了阿里云的国内镜像以加快速度) \n 1. Download Remote Indexes: True\n1. 最后点击底部的\"Save\"\n\n![nexus私服配置maven中央仓库缓存](./nexus-maven-central-cache.png \"nexusa中央仓库缓存配置\")\n\n缓存的时间比较长，在我的虚拟机上，花费了20分钟。进度可以在这里查看，左侧菜单\"Administration\" -> \"Logging\" 选择Log, 可以看到目前还在缓存:\n```shell\n2018-05-28 08:40:19,792+0000 INFO  [pxpool-1-thread-1] admin org.sonatype.nexus.index.DefaultIndexerManager - Trying to get remote index for repository \"Central\" [id=central]\n```\n\n等缓存成功后，在本地仓库的\"Browse Index中\"，应当能看到与中央仓库一样的目录结构，如下图所示：\n\n![nexus私服中查看中央仓库结构](./nexus-maven-central-cache.png \"nexus私服中查看仓库结构\")\n\n至此，我们成功架设了基于Nexus的Maven私有仓库，集成了LDAP登录，并缓存了Maven中央仓库。\n\n## 如何在Gradle中应用私有仓库\n\n在配置了私有仓库后，我们还需要在微服务项目中启用这个私有仓库。\n\n这大致需要2步\n1. 配置maven私有仓库用户名和密码\n1. build.gradle中配置\n\n下面我们分别看一下\n\n## 配置Maven私有仓库用户名和米按摩\n\n```shell\nvim ~/.m2/settings.xml\n\n# 新增如下内容\n<settings>\n    <servers>\n        <server>\n            <id>nexus_coder4</id>\n            <username>lihy</username>\n            <password>pass</password>\n        </server>\n    </servers>\n    <mirrors>\n        <mirror>\n            <!--This sends everything else to /public -->\n            <id>nexus_coder4</id>\n            <mirrorOf>central</mirrorOf>\n            <url>http://192.168.99.100:8081/content/groups/public</url>\n        </mirror>\n    </mirrors>\n    <profiles>\n        <profile>\n            <id>nexus_coder4</id>\n            <!--Enable snapshots for the built in central repo to direct -->\n            <repositories>\n                <repository>\n                    <id>central</id>\n                    <url>http://192.168.99.100:8081/content/groups/public</url>\n                    <releases><enabled>true</enabled></releases>\n                    <snapshots><enabled>true</enabled></snapshots>\n                </repository>\n            </repositories>\n            <pluginRepositories>\n                <pluginRepository>\n                    <id>central</id>\n                    <url>http://192.168.99.100:8081/content/groups/public</url>\n                    <releases><enabled>true</enabled></releases>\n                    <snapshots><enabled>true</enabled></snapshots>\n                </pluginRepository>\n            </pluginRepositories>\n        </profile>\n    </profiles>\n    <activeProfiles>\n        <activeProfile>nexus_coder4</activeProfile>\n    </activeProfiles>\n</settings>\n\n```\n如上，我们新增了私有仓库的地址、用户配置，如果你觉得在文件中直接\"裸写\"密码不安全，可以参考[maven密码加密方法](https://maven.apache.org/guides/mini/guide-encryption.html#How_to_encrypt_server_passwords)。\n\n下面，我们在build.gradle中配置：\n```build.gradle\nbuildscript {\n\n    repositories {\n        maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }\n        maven { url 'https://jitpack.io' }\n    }\n\n    dependencies {\n        // version just for plugin, not important\n        classpath(\"org.springframework.boot:spring-boot-gradle-plugin:1.5.6.RELEASE\")\n    }\n}\n\nsubprojects {\n\n    apply plugin: 'java'\n    apply plugin: 'idea'\n    apply plugin: 'maven'\n    apply plugin: 'org.springframework.boot'\n\n    sourceCompatibility = 1.8\n    targetCompatibility = 1.8\n\n    group = 'com.coder4.lmsia'\n    version = '0.0.1'\n\n    repositories {\n        maven {\n            credentials {\n                username \"$mavenUser\"\n                password \"$mavenPass\"\n            }\n            url 'http://192.168.99.100:8081/nexus/content/groups/public'\n        }\n        mavenLocal()\n    }\n\n    // maven deploy config start\n    configurations {\n        deployerJars\n    }\n\n    uploadArchives {\n        repositories.mavenDeployer {\n            configuration = configurations.deployerJars\n            repository(url: \"http://192.168.99.100:8081/nexus/content/repositories/releases/\") {\n                authentication(userName: \"$mavenUser\", password: \"$mavenPass\")\n            }\n            snapshotRepository(url: \"http://192.168.99.100:8081/nexus/content/repositories/snapshots/\") {\n                authentication(userName: \"$mavenUser\", password: \"$mavenPass\")\n            }\n        }\n    }\n\n    // maven deploy config end\n\n}\n```\n\n如上，build.gradle主要进行如下配置:\n* 子项目的仓库，采用私有仓库\n* 子项目发布包时，也发布到私有仓库上\n\n至此，我们成功地将maven私有仓库应用到了gradle的微服务上。\n"
  },
  {
    "path": "legacy/toolchain/spring-boot-scripts.md",
    "content": "# 懒人脚本\n\n"
  },
  {
    "path": "legacy/toolchain/spring-boot-template.md",
    "content": "# Spring Boot 项目模板\n\n"
  },
  {
    "path": "legacy/toolchain/stress-test.md",
    "content": "# 打压工具\n\n当业务刚刚起步的时候，微服务的稳定性是我们的首要保障目标，即服务能否稳定运行而不会挂掉。\n\n随着业务逐渐发展，用户数据量不断增大，并发的请求数也会不断加大。慢慢地，性能问题也会逐渐暴露出来。\n\n典型的性能问题有:\n1. 服务响应变慢\n1. 并发请求过多，导致数据库连接打满，无法访问数据库\n1. 流量过大，带宽被打满\n\n想要解决性能问题，先要能客观的评价性能，例如：在100个并发用户的前提下，我们的服务每秒能处理多少请求？\n\n要评估这类性能问题，就需要做一些性能压测。\n\n性能打压工具可以大致分为两类：\n1. 在UI界面配置完成打压，如JMeter、Tsung等\n1. 需要写代码完成打压，如Gatling、Locust等\n\n对于JMeter等工具，虽然上手简单，但是可定制程度较低，一些复杂的规则和参数配置起来很繁琐，一般由测试人员做简单打压时使用。\n而Locust等工具，虽然需要写代码，但了都提供了简化的API，编写起来非常简单，而且可以适应复杂的业务需求。\n\n综上所述，我们选用代码类打压工具。\n\nGatling性能很好，但只支持Scala语言；Locust是Python语言开发的，可以支持多种编程语言。在本书中，我们将分别介绍这两款打压工具。\n\n## 暴露服务端口\n\n在介绍打压工具前，我们先要对服务进行一些变更，让服务能够从集群外访问到。\n\n在[Spring Boot整合REST服务](spring-boot-1/sb-rest.md)章节中，我们配置了基于Kubernetes的REST服务，并设置了虚拟IP、虚拟IP的8080端口负责多结点的负载均衡。但是，虚拟IP默认只在集群内部生效。\n\n当我们需要将Kubernetes服务暴露给集群外时，一般有如下选择:\n* 为Service添加NodePort\n* 为Service添加ClusterIP\n* 增加Nginx反向代理，并为Nginx添加上述外部暴露的端口\n\n在这里，我们采用第一种方式，如果你想了解其他方式，可以参考[Publising Service](https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types)\n\n看一下更新的service描述文件\n```yaml\napiVersion: v1                                                                                           \nkind: Service\nmetadata:                                                                                                \n  name: lmsia-abc-server-service\nspec:                                                                                                    \n  selector:                                                                                              \n    app: lmsia-abc-server\n  type: NodePort\n  ports:                                                                                                 \n  - name: http\n    protocol: TCP                                                                                        \n    port: 8080     \n    nodePort: 30888\n  - name: rpc\n    protocol: TCP                                                                                        \n    port: 3000\n    nodePort: 30999\n\n```\n\n与之前的文件相比，上述yaml描述主要是增加了NodePort的定义和描述。\n* http端口对外暴露的是30888\n* rpc端口对外暴露的是30999\n\n我们应用下配置变更:\n```shell\nkubectl apply -f lmsia-abc-server-service-node-port.yaml\n```\n\n\n然后尝试访问，可以成功访问:\n```shell\ncurl http://192.168.99.100:30888/lmsia-abc/api/\n\nHello, REST\n```\n\n## Locust打压工具\n在你的开发机上安装\n```shell\npip install locustio\n```\n\n下面我们来看打压脚本hello.py:\n\n```python\nfrom locust import HttpLocust, TaskSet, task\n\nimport resource\nresource.setrlimit(resource.RLIMIT_NOFILE, (999999, 999999))\nprint resource.getrlimit(resource.RLIMIT_NOFILE)\n\nclass TestSet(TaskSet):\n    @task(1)\n    def hello(self):\n        self.client.get(\"/lmsia-abc/api/\")\n\nclass WebsiteUser(HttpLocust):\n    task_set = TestSet\n    min_wait = 5000\n    max_wait = 9000\n\n```\n\n上面的代码非常简单，就是访问地址\"/lmsia-abc/api/\"。\n\n下面来运行打压工具\n```\nlocust -f hello.py --host=http://192.168.99.100:30888\n```\n\n启动后，访问localhost:8089，会进入如下界面：\n\n![Locust选择用户数](./locust-swarm.png \"Locust选择用户数\")\n\n上面是设置最终多少并发，下面是设置用户增长的速度（每秒新增多少）。\n\n我们这里分别设置1000和200，然后点击开始。\n\n之后会进入打压进度页面，如下图所示：\n\n![Locust打压进度](./locust-progress.png \"Locust打压进度\")\n\n点击\"Charts\"，可以看到随着用户数变化，响应时间、QPS的变化曲线，如下图：\n\n![Locust打压变化](./locust-charts.png \"Locust打压变化\")\n\n打压结束后，点击\"STOP\"即可。\n\n除了单击打压外，Locust还支持分布式打压，即可以启动若干个节点共同完成打压作业，具体可以参考官方文档[Running Locust distributed](https://docs.locust.io/en/stable/running-locust-distributed.html)。\n\n## Gatling打压工具\n\n首先，到[官网](https://gatling.io/download/)下载最新版的gatling：\n\n```shell\nhttps://repo1.maven.org/maven2/io/gatling/highcharts/gatling-charts-highcharts-bundle/2.3.1/gatling-charts-highcharts-bundle-2.3.1-bundle.zip\n```\n\n然后解压缩到本地路径:\n```shell\nunzip gatling-charts-highcharts-bundle-2.3.1.zip\n\nmv gatling-charts-highcharts-bundle-2.3.1 gatling\n```\n\n然后看一下打压脚本:\n```scala\nimport io.gatling.core.Predef._ \nimport io.gatling.http.Predef._\nimport scala.concurrent.duration._\n\nclass HelloSimulation extends Simulation {\n\n  val httpConf = http \n    .baseURL(\"http://192.168.99.100:30888\") \n\n  val scn = scenario(\"HelloSimulation\").during(30) {\n    exec(http(\"hello_1\")\n    .get(\"/lmsia-abc/api/\"))\n  }\n\n  setUp(\n    scn.inject(atOnceUsers(2000))\n  ).protocols(httpConf)\n}\n\n```\n\n如上，Gating的打压脚本稍微复杂一些:\n* 服务根地址192.168.99.100:30888\n* 访问的get请求\"/lmsia-abc/api/\"\n* 并发2000个用户\n\n执行一下打压：\n```shell\ngatling.sh -sf . -s HelloSimulation\n```\n\n结果中可以直接看到各项统计结果：\n```shell\n---- Global Information --------------------------------------------------------\n> request count                                     353505 (OK=353505 KO=0     )\n> min response time                                      0 (OK=0      KO=-     )\n> max response time                                   1644 (OK=1644   KO=-     )\n> mean response time                                   170 (OK=170    KO=-     )\n> std deviation                                        122 (OK=122    KO=-     )\n> response time 50th percentile                        143 (OK=143    KO=-     )\n> response time 75th percentile                        219 (OK=219    KO=-     )\n> response time 95th percentile                        408 (OK=408    KO=-     )\n> response time 99th percentile                        606 (OK=606    KO=-     )\n> mean requests/sec                                11047.031 (OK=11047.031 KO=-     )\n---- Response Time Distribution ------------------------------------------------\n> t < 800 ms                                        353348 (100%)\n> 800 ms < t < 1200 ms                                 148 (  0%)\n> t > 1200 ms                                            9 (  0%)\n> failed                                                 0 (  0%)\n================================================================================\n\nReports generated in 2s.\n\n```\n\nGating的打压工具功能更为强大，具体可以参考官方教程[Gatling UserGuides](https://gatling.io/docs/2.3/)\n"
  },
  {
    "path": "src/README.md",
    "content": "# 从0到1实战微服务架构(第2版)\n\n## 地址汇总\n\n* [Github项目 求Star:-)](https://github.com/liheyuan/hands-on-microservices)\n* [在线阅读](https://coder4.com/homs_online/)\n\n## 第2版前言\n\n自从本书发布了后，技术圈发生了许多变化：\n\n* Spring Boot 2.X 稳定版发布\n* Kubernetes下的包管理项目“Helm”，正式加入CNCF基金会\n* 阿里巴巴开源了Nacos服务发现项目\n* ......\n\n3年后的2021年，我正式开启了本书2.0版的写作计划。\n\n由于技术更新迭代频繁，这是一次完全的重写，不是修订。\n\n由于gitbook项目已不再维护，我改用[mdBook](https://github.com/rust-lang/mdBook)做为渲染工具，[MarkText](https://github.com/marktext/marktext)做为写作工具。\n\n写作水平有限，还请各位多提宝贵意见。\n\n## 第1版前言\n\n微服务是继SOA后，最流行的服务架构风格之一。\n\n按照微服务对系统进行拆分后，每个服务的业务逻辑都更加简单、清晰。服务之间是松耦合的，模块之间的边界也更加清晰。\n\n微服务有效降低了软件项目的业务复杂程度，为小团队独立开发、持续交付和部署打下了良好的基础。\n\n遗憾的是，微服务并不是银弹。与传统的单一架构相比，微服务架构对团队的组织架构、技术水平、运维能力等方面，都提出了更高的要求。如果没有掌握得当的方法而生搬硬套，微服务架构只会会适得其反－－降低项目的开发效率，这是本书的创作初衷之一。\n\n在国内外的技术社区中，比较推崇现有开源方案，如\"Spring Cloud全家桶\"或者阿里开源的\"Dubbo\"。\n\n上述框架通常已经实现了服务发现、配置、负载均衡、限流熔断，等微服务架构所必须的的核心功能。\n\n使用开源框架省却了造轮子的过程，但也降低了我们学习、思考的动力。\n\n为什么需要服务发现，又如何实现它呢？配置中心呢....思考和设计的过程充满了挑战，也是提升自身架构能力的一种手段。这是本书的创作初衷之二。\n\n已有的微服务资料过于重视微服务的开发，忽略了微服务赖以生存的生态系统：工具链、自动化运维。可以说，离开了这两点的支持，微服务架构将难以落地。完善这两方面的思考和实战，是本书的创作初衷之三。\n\n为此，我撰写了这本《从0到1实战微服务架构》。让我们\"暂时忘掉\"已有的、成熟的开源解决方案。尝试亲自动手，实现微服务架构的各个模块。\n\n我们会从微服务开发、工具链、运维这三个角度，阐述微服务架构的实战方案。\n\n如果本书帮助了你，欢迎在在[github](https://github.com/liheyuan/hands-on-microservices)加Star，但严禁用于商业用途！(参见本页底部版权声明)\n\n由于能力水平所限，本书难免存在各种错误，恳请各位进行指正(Issue or PR)，谢谢！\n\n## 读者基础\n\n由于篇幅、精力所限，本书无法写成一本”零起点”教程。我假设读者具有至少2年的服务端工作经验，并且了解以下技术或原理：\n\n* Git\n* Maven & Gradle\n* Docker & Kubernetes\n* Java\n* Spring / Spring Boot \n* 数据库: 如MySQL\n* 消息队列: 如RabbitMQ\n* 缓存系统: 如Memcached \n* 内存数据库: 如Redis\n\n本书可以供架构师、项目经理、高级服务端程序员参考、学习。\n\n动手实战是本书的核心内容，因此本书所涉及的全部代码，都托管到了我的[Github上](https://github.com/liheyuan)(以lmsia-开头的项目)。\n\n这些代码以研讨为主要目的，也可以直接应用于生产，但本人不对其稳定性负责。\n\n## 版权\n\n本书虽然在github上公开写作，但版权归本人[Coder4](https://coder4.com)所有。\n\n依照 [署名-非商业性使用-相同方式共享](https://creativecommons.org/licenses/by-nc-sa/2.5/cn/) ，任何人可以在保留署名的情况下转载。但严禁用于商业用途。\n\nThis is a book powered by [mdBook](https://github.com/rust-lang/mdBook).\n"
  },
  {
    "path": "src/SUMMARY.md",
    "content": "# [从0到1实战微服务架构](./README.md)\n\n- [前言](./README.md)\n\n- [微服务概述](./ch01-architecture/micro-service-intro.md)\n  \n  - [微服务研发工具链](./ch01-architecture/rd-ops-toolchain.md)\n  - [持续集成、持续部署、持续交付](./ch01-architecture/continuous-x.md)\n  - [一种微服务的分层架构](./ch01-architecture/ms-architecture.md)\n  - [一种微服务分层架构的技术栈选型](./ch01-architecture/ms-tech-stack.md)\n\n- [微服务开发上篇](./ch02-ms-dev1/README.md)\n  \n  - [Gradle构建工具配置](./ch02-ms-dev1/gradle.md)\n  - [Sprint Boot项目与Gradle的集成](./ch02-ms-dev1/spring-boot.md)\n  - [Spring Boot集成SQL数据库1](./ch02-ms-dev1/database1.md)\n  - [Spring Boot集成SQL数据库2](./ch02-ms-dev1/database2.md)\n  - [Spring Boot集成gRPC框架](./ch02-ms-dev1/rpc.md)\n  - [Spring Boot集成Redis](./ch02-ms-dev1/redis.md)\n\n- [微服务开发中篇](./ch03-ms-dev2/README.md)\n  \n  - [Nacos注册中心：注册篇](./ch03-ms-dev2/registry1.md)\n  - [Nacos注册中心：发现篇](./ch03-ms-dev2/registry2.md)\n  - [Spring Boot集成配置中心](./ch03-ms-dev2/config.md)\n  - [Spring Boot集成熔断、限流、降级](./ch03-ms-dev2/circuit-breaker-and-limiter.md)\n  - [Spring Boot集成消息队列](./ch03-ms-dev2/mq.md)\n\n- [微服务开发下篇](./ch04-ms-dev3/README.md)\n  \n  - [基于ELKFK打造日志平台](./ch04-ms-dev3/elkfk.md)\n  - [基于SkyWalking的链路追踪系统](./ch04-ms-dev3/skywalking.md)\n  - [基于MicroMeter实现自定义应用监控指标](./ch04-ms-dev3/micrometer.md)\n  - [基于VictoriaMetrics + Grafana的监控系统](./ch04-ms-dev3/victorialmetrics.md) \n\n- [容器与编排系统](./ch05-k8s/README.md)\n  \n  - [从集装箱到容器](./ch05-k8s/container.md)\n  - [快速入门Kubernetes](./ch05-k8s/k8s-101.md)\n  - [搭建Kubernetes集群](./ch05-k8s/k8s-cluster.md)\n  - [搭建Kubernetes高可用集群](./ch05-k8s/k8s-ha-cluster.md)\n  - [通过ingress暴露内部服务](./ch05-k8s/k8s-ingress.md)\n\n- [持续交付流水线](./ch06-cd/README.md)\n  \n  - [Jenkins搭建入门](./ch06-cd/jenkins.md)\n  \n  - [Jenkins定制Agent](./ch06-cd/jenkins-custom.md)\n  \n  - [Jenkins实现Kubernetes部署流水](./ch06-cd/jenkins-k8s.md)\n  \n  - [Jenkins优化Kubernetes部署流水线](./ch06-cd/jenkins-k8s-optimize.md)\n\n- [工具链](./ch07-tools/README.md)\n  \n  - [基于LDAP的内网统一认证](./ch06-cd/jenkins.md)\n  - [JFrog Artifactory搭建Maven私有仓库](./ch07-tools/ldap.md)\n  - [使用Registry2搭建Docker私有仓库](./ch07-tools/registry2.md)\n"
  },
  {
    "path": "src/ch01-architecture/README.md",
    "content": "# 第1章 微服务架构概述\n\n当我们讨论服务端的架构时，“微服务”已经成为了最热门的关键字。\n\n如果没有接触过\"微服务\"，那么你的心里一定存在很多号？\n\n不要急，我们将从三个基本问题谈起：\n\n- 什么是“微”服务?\n\n- 为什么需要微服务?\n\n- 微服务是“银弹”么？\n\n接着，我们将沿着：\n\n- 研发工具链\n\n- 微服务架构\n\n两个线索展开，最后将讨论技术选型。\n\n让我们开始“微”服务的探索之旅吧:-)\n"
  },
  {
    "path": "src/ch01-architecture/continuous-x.md",
    "content": "# 持续集成、持续部署、持续交付\n\n标题里的三个“持续”在前几年特别火热，属于技术热词(BuzzWord)。\n\n持续交付(Continuous Delivery)由马丁·福勒（Martin Fowler）于2006年提出。\n\n是的，你没看错，又是马丁·福勒，那位提出微服务的大神。\n\n歪个楼，介绍一些马丁·福勒的代表作：\n\n- 《重构：改善既有代码的设计》\n\n- 《企业应用架构模式》\n\n- 《敏捷软件开发宣言》(联合)\n\n- “微服务”、“持续部署” ....\n\n以上任何一条单独拿出来，都足以封神。\n\n言归正传，我们在一本“微服务”的书中讨论持续交付，仅仅因为它是由大神提出的么？\n\n当然不是，我们将在本文的末尾再讨论这个问题。\n\n[这篇](https://www.mindtheproduct.com/what-the-hell-are-ci-cd-and-devops-a-cheatsheet-for-the-rest-of-us/)文章很好的阐述了三个概念的联系与区别，我们展开讨论。\n\n## 持续集成\n\n小王每次向gitlab提交一个代码，就会触发一次项目的自动构建、运行单元测试，这就是持续集成(Continuous Integration)。如下图所示：\n\n![](./ci.png)\n\n假设小王在提交中引入了一个Bug，借助CI流程(中的集成 or 单元测试)，我们就能在第一时间发现，并尽早修复问题。\n\n管理学大师戴明指出：“问题发现的越早，修复的成本越低”。通过持续集成，我们可以尽早发现问题，从而降低(修复问题带来的)返工成本。\n\n## 持续部署\n\n持续部署(Continuous Deployment)指的是：在持续集成(成功)的基础上，自动将服务部署到\"类似于线上\"的环境中，如下图所示：\n\n![](./cd.png)\n\n为什么要部署到\"类似于线上环境\"呢？因为代码只在\"集成阶段\"通过了一部分\"单元测试\"，假设单元测试覆盖不全，甚至还需要人工测试，那就可能将隐含的Bug发布到线上，造成生产事故。\n\n图中画的\"TEST\"(测试环境)、\"STAGING\"(预发环境)，都是这类\"类似线上环境\"。当新版本通过最终确认后，再手动(MANUAL)部署到线上。\n\n## 持续交付\n\n持续交付(Continuous Delivery)在持续部署的基础上，更近了一步：成功发布到\"类似生产环境\"后，会继续自动发布到线上，如下图所示：\n\n![](./cd2.png)\n\n显然，这种\"自动发布\"需要极强的自信和勇气。这可能源于充分的单元测试，清晰的架构，以及对业务能力的自信。\n\n实际上业界只有极少数公司\"从容地\"实现了上述意义上的\"持续交付\"。\n\n其余宣称实现了\"持续交付\"的公司，或者混淆了持续部署的概念，或者对技术故障存在较大容忍度。 (先发布再灰度，难道不是一种容忍?)\n\n这并不是高级黑，如果你认真做过一段时间软件开发，应该能明白“即使100%的单测覆盖率，也不能自动检查出尚未发现的Bug”，更何况绝大多数项目根本无法达到100%单元覆盖率。\n\n我们回到本文开头的问题：为什么要在一本“微服务”的书中，讨论持续部署？\n\n还记得微[服务概述](./micro-service-intro.md)一节中，微服务的缺点么？可靠性陷阱、运维复杂度升高。\n\n- 借助持续集成，能够尽早发现缺陷，提升微服务架构下的可靠性。\n\n- 应用持续部署，可以上线效率，降低运维难度。\n\n由此可见，持续集成、持续部署，能够切实解决微服务中存在的问题。我们将在本书的后续章节，打造自己的持续集成系统，敬请期待。\n"
  },
  {
    "path": "src/ch01-architecture/micro-service-intro.md",
    "content": "# 微服务概述\n\n## 什么是“微”服务？\n\n如果你仔细观察，会发现我在上一行的标题中，将“微”打了个引号。\n\n如果我们暂时去掉这个''微\"字理解，微服务就是我们熟知的“服务端” 或者 “后端”。\n\n现在让我们把微字加回来:-)\n\n\"微服务\"(Microservices)由马丁·福勒（Martin Fowler）提出的一种架构理念，[原文]([Microservices](https://martinfowler.com/articles/microservices.html))发表于2014年。\n\n> 微服务是一种架构模式或者说是一种架构风格，它提倡将单一应用程序划分成一组小的服务，每个服务运行独立的自己的进程中，服务之间互相协调、互相配合，为用户提供最终价值。\n\n我们抓三个关键点来理解：\n\n- 单一应用划分为一组更小的服务：将一个较大的、复杂的应用，拆分为多个小的服务。你可能会问：“这样不会增加复杂度么”？是的，会增加。但这种拆分也会带来明显的优点，我们后面会提到。\n\n- 独立的进程：每个微服务独立运行在自己的进程中，互不干扰。虽然这里并没有限制进程的部署方式，但可以想见，经过\"划分\"后的微服务，势必会产生众多进程。微服务是拆分而来的，他们之间势必存在逻辑的耦合。由此，会产生新的问题\"微服务间的通信\"。\n\n- 相互协调、配合：微服务的进程间需要通信、交互。从理论而言，所有IPC(Inter-Process Communacation，进程间通信)的方式都可以完成这个过程。但微服务的进程众多，很难完整地部署在同一台机器上，这势必产生跨主机的网络通信。所以，在微服务中，多采用RPC(Reomote Procedure Call，远程过程调用)的方式来完成通信。\n\n![](./sketch.png)\n\n上图展示了单体服务 和 微服务的区别。\n\n## 为什么需要微服务？\n\n在前文中，我们挖了一个坑：'微服务的划分会导致复杂度上升'，为什么还要使用一项有缺陷的技术呢？\n\n我们先讲第一个故事。\n\n小张入职了一家互联网创业公司，一开始只有3个后端程序员，每天的工作是：和产品经理讨(si)论(bi)需求、写代(b)码(ug)，改Bug，工作紧张但规律。服务端的上线窗口是周五下午：合并分支、代码Review、推送线上，一气呵成，不仅能准点下班，还能去吃个火锅。\n\n过了两个月，行业赶上了风口，公司的业务快速发展，后端团队也快速膨胀到20人。然而，麻烦也接踵而至：大家修改的是同一个仓库下的服务端代码，\"解冲突\"成为了家常便饭，还发生了几次\"一个小修改，破坏了其他业务主流程“的严重线上事故。\n\n为了改善这种情况，老板招聘了2位QA(质量保证，测试)人员，由他们负责测试工作。然而，一个很小的改动都需要对整个后端服务的case进行全量回归测试。一个功能的开发需要1天，测试却耗费1周，迫于老板的压力，研发同学只能安慰自己：XX功能简单，不需要测试了，直接上线。\n\n最终，周五成为了\"噩梦日\"：周四晚上要提前开一个Excel表、统计好第二天要上线的需求，并按优先级排定顺序。周五全员提前1小时来公司，开始逐个逐个需求的\"合并代码\"”、\"解冲突\"，“上线”、\"观察\" 、“回滚”、“修改代码”...... \n\n上线结束的收工时间从6点变成了9点，又逐渐拖到了11点，最后索性全员加班、通宵上线。技术团队的每一位同学，都感到身心俱疲。\n\n听完这个故事，你是否有\"似曾相识\"的感觉？\n\n科普一下，上述故事中的服务一般称作“单体服务” 或者 “巨石服务”(Monoliths)。\n\n接下来，是第二个故事。\n\n由于工作强度大、线上故障频发、团队士气低落，老板请来了老刘担任技术经理。\n\n第一周：老刘带领团队将复杂、臃肿的\"巨型服务\"拆分成了“用户”、“订单”、“服务”三个微服务。\n\n第二周：老刘将团队进行了上述类似的拆分，也分成了三个小组。\n\n第三周：事情有了微妙的变化。分组后，合并代码引发的冲突减少了。开发业务时，多数的改动都封闭在单独的微服务内，改动造成的影响范围减少了，测试周期缩短了。\n\n......\n\n三个月后的一个周五的下午，（得益于提高的交付质量，以及微服务的独立并行上线），团队提前2小时完成了上线，距离上一次故障通报已经过去了两个月。\n\n研发讨论群里，小张发了一条消息：“今天居然可以正点下班了，老刘真厉害！”\n\n老刘回复：这是大家的努力的结果，真正“厉害”的应该是“微服务”。\n\n听完这两个故事，我们来总结下微服务架构的两个优点：）\n\n- 逻辑清晰：一个微服务只负责一项(或少数几项)很明确的业务，逻辑更加简介清晰，易于理解。\n\n- 独立自治：每个微服务由一个小组负责。减少了跨团队的代码冲突，同时降低了改动的影响范围，提高了研发效率。\n\n在故事之外，微服务架构还具有以下的优点：\n\n- 伸缩性强：相对于庞大的巨石服务，微服务更加独立，可以针对不同的性能需求，有选择的对不同微服务进行伸缩。举个栗子：明天有大促，产品预测：注册功能提升10倍，其他功能无波动。针对巨石服务，我们只能整体扩容10倍；微服务架构下，我们只需要10倍扩容用户微服务。\n\n- 技术异构性：每个微服务内可以使用不同的技术栈，甚至不同的开发语言。只要微服务之间使用统一的通信方式即可。\n\n微服务架构有很多优势，那团队抓紧上马微服务吧？\n\n## 微服务是“银弹”么？\n\n直接泼一盆冷水：\n\n> There is no Silver Bullet.  -- 《人月神话》\n\n微服务不是“银弹”，它存在以下缺点：\n\n- 复杂度升高：在巨石服务中，所有修改都集中在同一个项目内；在微服务架构下，复杂功能的开发，需要同步修改多个微服务，复杂度骤然升高。\n\n- 性能损耗：原本在巨石服务中的方法调用，演变为微服务之间的跨进程、网络通信。性能会受到较大影响。\n\n- 可靠性陷阱：假设每个服务的可靠性都是99%，一个巨石服务，可靠性是99%、三个微服务的可靠性会下降到99% x 99% x 99% = 97%。\n\n- 运维难度加大：巨石服务被拆分成N个微服务，部署的数量翻倍的增长。此外，多组微服务的运行，也会增大运维、监控的难度。\n\n有意思的是：\"拆分\"带来了优点，也引入了缺点。\n\n> 夫尺有所短，寸有所长，物有所不足，智有所不明。 -- 《楚辞．屈原．卜居》\n\n微服务架构也是如此，它的优缺点并存。\n\n## 微服务适用什么场景？\n\n什么场景适用微服务，什么场景不适用呢？\n\n这篇文章[《When to use and not use microservices》]([Best of 2020: When To Use - and Not To Use - Microservices - Container Journal](https://containerjournal.com/topics/container-ecosystems/when-to-use-and-not-to-use-microservices/))给出了一些建议：\n\n适用微服务架构的场景：\n\n- 希望巨石服务能适应“可扩展性”、“敏捷性”、“可管理性”，提升交付速度时\n\n- 需要为(使用陈旧技术开发的)的老系统，迭代新功能时\n\n- 有一些相对独立的模块可以跨业务复用时：如登录、检索、身份验证等。\n\n- 构建需要快速交付、创新度高、敏捷的应用 / 服务\n\n不适用微服务架构的场景：\n\n- 业务简单，无需处理复杂问题\n\n- 团队规模太小，尚无法负担微服务拆分带来的复杂度提升\n\n- 为了微服务而微服务\n\n最后，引用马丁·福勒（Martin Fowler）论文的结尾做结束本节的讨论。\n\n> 我们怀着谨慎、乐观的态度写了这篇文章。到目前为止，我们已经看到：微服务风格是一条非常值得探索的路。我们不能肯定地说，我们将在哪里结束，但软件开发的挑战之一是，你只能基于目前能拿到手的、不完善的信息作出决定。\n"
  },
  {
    "path": "src/ch01-architecture/ms-arch.plantuml",
    "content": "@startuml\n\ntitle \"微服务架构实现\"\n\npackage \"聚合接入层\" as l5 {\n    [聚合服务1] as n51\n    [聚合服务2] as n52\n    [聚合服务3] as n53\n    [PaddingPadding] as n54\n    hide n54\n\n    [n51] -[hidden]right-> [n52]\n    [n52] -[hidden]right-> [n53]\n    [n53] -[hidden]right-> [n54]\n}\n\npackage \"业务服务层\" as l4 {\n    [微服务1 ] as n41\n    [微服务2] as n42\n    [微服务3] as n43\n    [微服务4] as n44\n    [Pa] as n45\n    hide n45\n\n    [n41] -[hidden]right-> [n42] \n    [n42] -[hidden]right-> [n43]\n    [n43] -[hidden]right-> [n44]\n    [n44] -[hidden]right-> [n45]\n}\n\npackage \"微服务设施层\" as l3 {\n    [开发框架] as n31\n    [RPC] as n32\n    [服务注册发现] as n33\n    [配置中心] as n34\n    [ ] as n35\n    hide n35\n    \n\n    [熔断/限流] as n36\n    [数据库] as n37\n    [NoSQL] as n38\n    [消息队列] as n39\n\n    [链路追踪] as n3a\n    [监控] as n3b\n    [报警]  as n3c\n    [日志] as n3d\n\n\n    [n31] -[hidden]right-> [n32]\n    [n32] -[hidden]right-> [n33]\n    [n33] -[hidden]right-> [n34]\n    [n34] -[hidden]right-> [n35]\n\n    [n31] -[hidden]down-> [n36]\n\n    [n36] -[hidden]right-> [n37]\n    [n37] -[hidden]right-> [n38]\n    [n38] -[hidden]right-> [n39]\n\n\n    [n36] -[hidden]down-> [n3a]\n\n    [n3a] -[hidden]right-> [n3b]\n    [n3b] -[hidden]right-> [n3c]\n    [n3c] -[hidden]right-> [n3d]\n}\n\n\npackage \"运维平台层\" as l2 {\n    [CI/CD系统] as n21\n    [部署版本系统] as n22\n    [容器调度系统] as n23\n    [PaddingP] as n24\n    \n    hide n24\n\n    n21 -[hidden]right-> n22 \n    n22 -[hidden]right-> n23\n    n23 -[hidden]right-> n24\n}\n\n\n\npackage \"基础设施\" as l1 {\n    [计 算 资 源] as n11\n    [存 储 资 源] as n12\n    [网 络 资 源] as n13\n    [PaddingPPddi] as n14\n\n    hide n14\n\n    n11 -[hidden]right-> n12\n    n12 -[hidden]right-> n13\n    n13 -[hidden]right-> n14\n}\n\nn51 -[hidden]down-> n41 \nn41 -[hidden]down-> n31\nn3a -[hidden]down-> n21\nn21 -[hidden]down-> n11 \n\n\n@enduml"
  },
  {
    "path": "src/ch01-architecture/ms-architecture.md",
    "content": "# 一种微服务的分层架构\n\n在上一小节，我们讨论了微服务架构“的的特征、优缺点等话题。\n\n你可能对微服务有了一个模糊的概念，依然感觉不够清晰。\n\n这种感受能够理解。因为，微服务的理论只是提供了一种“架构风格”的建议，并不包含具体的实施方案。\n\n下图展示了一种微服务的分层架构：\n\n![微服务整体架构](./ms-arch.png \"微服务整体架构\")\n\n让我们自底向上、逐层分解：\n\n1. 基础设施层\n   \n   基础设施层涵盖了服务端运行时，所需要的物理资源。包括：计算资源、存储资源、网络资源等。\n   \n   针对小型公司，可以直接选用云计算平台的资源(如阿里云、AWS等)；中大型公司出于成本、审计等因素，会自建机房或混合云。\n   \n   计算资源：CPU、GPU、内存等。除了CPU的核数、内存容量，配比等常见问题，还需要考虑计算资源的弹性伸缩能力，即如何应对“平台大促”等场景带来的流量提升。\n   \n   存储资源：不仅要考虑磁盘容量，还要考虑磁盘性能([IOPS]([IOPS - 维基百科，自由的百科全书](https://zh.wikipedia.org/wiki/IOPS)))。举个例子：服务端日志主要是顺序写，异步处理 + 大容量机械磁盘就能满足要求；对MySQL等数据库场景，涉及大量随机读，使用SSD可以显著提升性能。\n   \n   网络资源：外网带宽(峰值)、内网带宽、负载均衡、VPC等。内外网带宽问题较为常见，我们不再讨论。负载均衡：当业务流量规模升高后，接入层的传统软负载解决方案(Nginx、LVS)会显得力不从心。硬件负载均衡(F5)可以提供更高的性能，但做为专用计算的商业产品，其价格在百万以上。这几年，随着Kernel By Pass技术的兴起，基于X86通用硬件 + Linux的的软负载均衡也取得一定的性能突破，感兴趣的话，可以参考这篇[文章]([从Maglev到Vortex，揭秘100G＋线速负载均衡的设计与实现-InfoQ](https://www.infoq.cn/article/Maglev-Vortex/))。\n   \n   基础设施层的技能栈主要是：运维、网络建设，我们不在本书中做更多讨论。\n\n2. 运维平台层\n   \n   运维平台层是“[持续交付](continuous-x.md)”的重要载体，包括：\n   \n   持续部署系统：构建从代码仓库、持续集成、持续部署的全链路系统、最终实现持续交付。\n   \n   部署的版本管理系统：管理部署镜像粒度的版本，以支持滚动发布、回滚等部署功能。\n   \n   容器、容器管理调度平台：容器是一种操作系统级的轻量虚拟化技术。在部署系统中，不仅需要容器技术、还需要容器调度管理系统。这两项技术我们会在后续章节展开讨论。\n\n3. 微服务设施层\n   \n   本层为微服务的开发和运行提供公用的设施基础。\n   \n   在这里我们只做基本介绍，在后续章节会详细展开。\n   \n   开发框架：微服务的开发需要一些基础的编程框架，可以自己从零搭建，也可以基于成熟开源框架完善。\n   \n   RPC：微服务内部使用RPC(Remote Procedure Call)完成通信。\n   \n   服务注册与发现：微服务A调用服务B且后者有3个实例，如何感知这3个实例的IP、端口，以及A要调用哪个实例呢？这就是服务的注册与发现问题，是微服务的核心问题之一。\n   \n   配置中心：微服务的数量、实例众多，逐一修改配置文件的传统模式，既不经济又容易出错。配置中心是一个中央(但不一定是单机)配置系统，负责配置管理、修改等工作。\n   \n   熔断：当微服务调链路上，服务不可用或响应时间太长时，触发熔断，快速提前返回。举个例子：家里有用电设备路时电流过大，空气开关会直接跳闸，防止造成进一步的破坏。\n   \n   限流：为了保护服务不被流量击垮，而提前限制流量。举个例子：经过测算，故宫接待能力是每日1万人。那么当天超过1万后，就触发限流，不让更多游客入园。\n   \n   数据库：传统的SQL数据库用于业务数据落盘，NoSQL数据库则用于缓存或高性能存取。\n   \n   消息队列：将业务流量“削峰填谷”，对应对突发流量。\n   \n   中间件：中间件是介于 服务端 与 数据库、消息队列等设施的中间。中间件帮助 业务服务更简单地使用这些基础设施。\n   \n   近几年，“可观测性”成为了新的技术热词。这个舶来于控制理论的词，在软件系统中指的是：可以帮团队有效调试系统的工具或解决方案。以这个视角看，下述部分都是可观测性的一部分：\n   \n   日志：如何在众多的微服务实例中，快速定位到某一种出错日志？日志平台实现了微服务实例中的日志收集、存储、检索、分析。\n   \n   监控系统：通过采集多种指标，实时反馈系统运行状态，保证服务的平稳运行。举个生活中的例子：汽车驾驶位的仪表盘。\n   \n   报警系统：当监控系统发现异常时，及时将报警发送出来。\n   \n   链路追踪：当服务A->B->C调用链上发生超时，如何快速定位哪个环节发生了故障？链路追踪解决了分布式、复杂调用链路中的采集、追踪，分析工作。\n\n4. 业务服务层\n   \n   借助“基础设施“、”运维平台“、”微服务设施“的帮助，我们可以更高效、稳健的应用微服务，实现业务目标。关于微服务的拆分、建模理论，可以参考“领域驱动设计”的相关内容，本书不做讨论。\n\n5. 聚合接入层\n   \n   在“[微服务概述](micro-service-intro.md)”一节中，我们曾提到微服务的缺点之一：拆分导致的复杂度升高。在当前主流的前后端分离架构中，用户对这一拆分基本无感知。复杂度被转嫁到 前端  / 客户端 中：原本只需要调用一个接口，现在要分别调用N个微服务。还需要考虑时序关系、错误处理等。聚合接入层就是为解决这个问题而生的，他聚合多个微服务的调用，只保留必要字段，为前端 / 客户端提供了统一、清晰的服务接口。聚合接入层可以由服务端实现，有时还会加入部分熔断、限流等逻辑，组合成为微服务网关。聚合接入也可以由前端实现，有时也被称作BFF(Backend For Frontend)。\n\n在剖析微服务的各层架构之后，不难发现：微服务的架构下，需要多个团队，多层系统、多纬度的支持。这也印证了在“[微服务概述](micro-service-intro.md)”一节中的观点：应用微服务架构，需要较高成本。\n\n因此，尽量选用成熟、易维护的技术，从而尽可能降低成本，显得尤为重要。我们将在下一节展开讨论技术选型。\n"
  },
  {
    "path": "src/ch01-architecture/ms-tech-stack.md",
    "content": "# 一种微服务分层架构的技术栈选型\n\n我们在[工具链](./rd-ops-toolchain.md)、[一种微服务的分层架构](./ms-architecture.md) 两小节中讨论了技术栈的需求。\n\n在本节中，我们将具体讨论技术栈的选型。\n\n你可能注意到，上一节的标题是“一种微服务的分层架构”，而这一节的标题是“一种微服务分层架构的技术栈选型”。\n\n加上“一种”这个词是有意而为之，请不要怀疑我的语文水平:-)\n\n\"一种\"强调的是：\n\n- 微服务只是一种架构风格，他可以有N种不同的实现，上一节只介绍了其中一种。\n\n- 每一种微服务架构的实现，也可以对应N种不同的技术栈选型。\n\n那么，在这N^2种架构 + 技术栈的组合种，哪一种才是最好的？\n\n不急着回答，我们先来看下这个：\n\n> php is the best language for web programming.\n\n这是PHP官方手册的原文，更多人更熟悉前5个单词，“PHP是全世界最好的语言”。\n\n但加上后3个单词“for web programming”后，就变成了“PHP是web领域最好的语言”。\n\n而我的观点(哪个架构更优) 与 PHP社区(关于语言优劣)的观点，是一致的：没有最好的语言(技术)，只有最适合具体场景的。\n\n因此，我们只会针对各项场景，列出技术选型，而不会打“为什么A比B好的”口水战。\n\n## 容器管理平台的技术选型\n\n微服务架构下会对服务进行拆分，产生大量的服务实例。\n\n容器化技术，可以实现环境隔离、快速部署，是微服务架构的基石。\n\nDocker凭借“快速”、“可移植性”等特性\"\"一战成名\"，是单机或小规模应用部署的最佳选择\n\n然而，在复杂的分布式部署场景中，\"扩容\"、\"编排\"、\"故障恢复\"等成为了\"刚需\"，“容器管理平台”应运而生。在这个赛道上，曾经出现过三个主流产品：\n\n- swarm: Docker公司于2014年末推出的容器集群技术方案。尽管swarm是Docker公司的“亲儿子”、手握大量社区资源，但很快被Kubernetes超越。\n\n- Kubernetes: 简称k8s，支持自动部署，扩展和管理容器化应用程序的开源系统。k8s借鉴了Google的Borg管理系统，自问世以来发展迅猛，当前已经成为了容器管理的事实标准。\n\n- Marathon: 构建在[Apache Mesos]([Apache Mesos](http://mesos.apache.org/))集群上的一套容器集群管理软件。由于Mesos的部署存在门槛，Marathon项目的关注度并不高，社区也并不活跃。其上一个发布版本依然停留在2019年，已经近2年没有更新。\n\n因此，我们\"毫无争议\"地选择k8s作为微服务架构下的容器管理平台。\n\n除了容器管理平台，我们还需要镜像仓库存储应用的容器镜像，我们将使用Docker搭建私有镜像仓库。\n\n## 微服务设施层的技术选型\n\n设施层涉及较多的技术需求，技术选型如下：\n\n| 需求               | 选型                        | 版本             |\n| ---------------- | ------------------------- | -------------- |\n| 开发语言             | Java                      | 8              |\n| 开发框架             | Spring Boot               | 2.5.4          |\n| RPC              | gRPC                      | 1.14.x         |\n| 服务注册 / 发现 / 配置中心 | Nacos                     | 2.x            |\n| 熔断 / 限流          | Resilience4j              | 1.7.1          |\n| SQL数据库           | MySQL                     | 8.0.X          |\n| 内存数据库            | Redis                     | 6.2            |\n| 消息队列             | RocketMQ                  | 4.9.1          |\n| 日志               | Kafka + ELK               | 2.13 + 7.14.X  |\n| 监控 / 告警          | VictoriaMetrics + Grafana | 1.64.1 + 8.1.X |\n| 链路追踪             | SkyWalking                | 8.7.0          |\n\n开发语言：我们选择了Java做为开发语言。与新近崛起的Go、Rust等语言相比，Java不是最完美的语言，但它依然拥有较高的开发、运行效率，最充足的人才供给。版本方面我们选择Java 8（最后一个免费的Java版本）。\n\n开发框架：在Java开发领域，Spring生态的渗透率已超过60% ([出处]([Spring dominates the Java ecosystem with 60% using it for their main applications | Snyk](https://snyk.io/blog/spring-dominates-the-java-ecosystem-with-60-using-it-for-their-main-applications/)))。顺应这一趋势，我们选择Spring 生态内的Spring Boot做为主要开发框架。Spring Boot提供的注解配置、嵌入式容器、starter等特性，可以极大简化Java应用的开发。\n\nRPC框架：我们选择开源的gRPC做为RPC框架，它使用Protocl Buffer序列化，HTTP 2传输协议，具有更灵活的通信模式和较高的传输效率。\n\n服务注册、发现、配置中心：[Nacos]([什么是 Nacos](https://nacos.io/zh-cn/docs/what-is-nacos.html))是阿里巴巴开源的服务管理项目，同时具备服务注册、发现、配置中心。Nacos原生支持Spring Boot、k8s等融合方向。经过几年的发展，Nacos已经较为成熟，支撑了阿里巴巴、中国移动等数十家大型公司的线上系统。\n\n熔断、限流：本书不会探讨Service Mesh等平台级别的流量控制方案。我们主要讨论服务进程级别的熔断、限流方案。老牌项目Hystrix停更后，我们选择开源的Resilience4j做为熔断、限流的Java库解决方案。\n\n数据库：做为开源数据库的佼佼者，MySQL常年稳居市场份额的前三名。我们选择其较新的稳定版8.0.X。\n\n内存数据库：做为SQL数据库的补充，内存数据库的应用场景是：吞吐量更大、延迟更低。高性能的Redis是最佳选择。根据[官方评测](https://redis.io/topics/benchmarks)，Redis 6.x在开启pipeline模式的前提下，可以提供高达55万RPS。\n\n消息队列：Apache RocketMQ是阿里巴巴的开源的分布式消息队列，具有极低的延迟和较高的吞吐量。相比于老牌的Kafka，Rocket MQ更适用于消息队列的场景。我们选用其最新稳定版4.9.1。\n\n日志：ELK是经典的日志日志方案。在此基础上，我们前置增加了Kafka，利用其强大的写能力，构建起缓冲队列，以应对海量日志的突发写入。\n\n监控 / 告警：纵观DevOps领域，Prometheus + Grafana已经成为了监控领域的事实标准。然而，Prometheus并不支持原生的集群部署，其在大规模应用下很容易出现瓶颈。[VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics)是一款可以嵌入Prometheus的分布式时序存储引擎。起初VictoriaMetrics只想做一个引擎，在近几个版本社区加大了对vmagent的开发投入。vmagent是一款轻量级的代理，兼容Prometheus协议，可以直接替代Prometheus完成大部分工作。在本书中，我们直接选择VictoriaMetrics + Grafana做为兼容告警的默认技术栈。\n\n链路追踪：[SkyWalking](https://skywalking.apache.org/)是由国人主导的一款开源APM(application performance management)。在小米、滴滴等公司都有应用。我们选择其最新的稳定版本。\n\n看了上面的文字，你可能有点困惑：“只是简单罗列选型结果，并没有具体分析过程“？\n\n技术选型是一个非常大的话题，每一个点单独拎出来，都能洋洋洒洒的写一章出来，但是我觉得必要性不大，原因在于：\n\n- 技术演进的速度非常快，今天适合的明天就有可能被淘汰(看看Docker)\n\n- 每个公司面临的具体场景情况都是不同的，很难穷尽、更无法全部都满足\n\n因此，我只是在自己可见的技术水平内，选择了相对靠谱的方案，解决了一部分“选择障碍的问题”，如果你有更优秀的选择，也欢迎提Issue交流、讨论。\n"
  },
  {
    "path": "src/ch01-architecture/rd-ops-toolchain.md",
    "content": "## 微服务研发工具链\n\n> 子曰：“工欲善其事，必先利其器。居是邦也，事其大夫之贤者，友其士之仁者。” \n> \n>                                                                                                                     -- 《论语》\n\n普通话版：工匠想要做好工作，先要把工具打磨锋利。\n\n程序员版：软件工程师要想写好代码，需要一把机械键盘，并定期清洗轴以维持最佳手感。\n\n对于程序员而言，除了键盘等硬件，还有一系列软件。我们这里将这些软件称为工具链。\n\n## 小王的一天\n\n下面，让我们跟随小张 - 是的，就是在风口创业公司的那位 - 看看在微服务架构下，研发工具链包含了哪些内容。\n\n| 时间    | 工作                             | 工具需求                           | 备注                     |\n| ----- | ------------------------------ | ------------------------------ | ---------------------- |\n| 09:01 | 打开浏览器，登录公司内网                   | 使用同一个账号，登录公司所有的内部系统。           | 暂不讨论“操作系统”、“浏览器”等通用软件。 |\n| 09:03 | 打开代码审核平台，查看Review              | 代码版本控制、代码托管，代码审核               |                        |\n| 10:23 | 老张让我升级下xx的包，加了新接口              | 版本依赖管理系统                       | 我们将开发语言暂时限定为Java       |\n| 11:56 | 修改了一部分逻辑，午饭前抓紧提交上去，看能否跑通所有Case | 持续集成(Continuous integration)系统 | 暂不讨论“IDE”等通用软件。        |\n| 15:20 | 功能开发完毕，上线！                     | 持续交付(Continuous delivery)系统    |                        |\n| 16:03 | X功能重构，拆分到两个微服务中                | 微服务开发辅助工具                      |                        |\n\n## 研发工具链\n\n小张的公司还处于创业阶段，出于节省成本的考虑，我们尽量选择开(mian)源(fei)的解决方案：\n\n1. 内部帐号统一管理：在企业的内部，存在许多内部系统。出于安全性、管理性的考虑，需要统一的帐号管理系统。这里我们选用[OpenLDAP](https://www.openldap.org/)：一款的开源的帐号管理服务，它实现了广泛使用的“轻量级目录管理协议”(LDAP v3)，可以轻松对接各类系统的帐号管理功能。\n\n2. 代码管理：团队协作的软件开发模式，需要版本控制系统。我们选用了[Git](https://git-scm.com/)做为代码的版本控制系统。在代码的托管、审核方面，[Gerrit](https://www.gerritcodereview.com/)和[GitLab](https://about.gitlab.com/install/?version=ce)都是成熟的开源解决方案。Gitlab上手容易，生态链更加成熟；Gerrit有一定上手门槛，在代码Review方面更加优秀。关于两者的讨论，可以参考这篇[帖子](https://www.reddit.com/r/git/comments/8ekeem/do_you_use_gerrit_software/)。经过多方面的综合考虑，我们选择了GitLab。\n\n3. 版本依赖系统：在Java开发中，[Maven](https://maven.apache.org/)是依赖管理的事实标准。同时在企业开发中，不希望将私有包发布到公开仓库中，我们选用[Nexus Repository OSS](https://www.sonatype.com/products/repository-oss)搭建私有的Maven仓库。\n\n4. 持续集成、持续交付，持续部署是三个既相近又重要的概念，我们将在下一小节展开讨论。\n\n5. 微服务辅助开发工具：在微服务架构下，新增微服务、升级pom版本，接口变更等操作会频繁发生。需要开发一些辅助工具，提升研发效率。我们会在后面展开讨论。\n\n针对上述选择的工具，我们会在后续章节详细介绍。\n\n## 微服务辅助开发工具\n\n结合微服务的开发特点，我们需要这样一些辅助工具：\n\n- 自动创建新的微服务：包括从模板项目生成微服务代码、自动创建git项目、部署项目\n\n- RPC桩文件生成：在RPC的(IDL)接口文件变更后，需要重新生成桩文件，这个步骤较为繁琐，需要工具辅助完成。\n\n- pom版本自动升级：微服务之间的版本依赖，更新会更加频繁，我们需要一个工具，自动修改pom版本\n\n这里我们只初步讨论一下需求，具体的实现会在后续章节展开。\n"
  },
  {
    "path": "src/ch02-ms-dev1/README.md",
    "content": "# 微服务开发上篇：开发框架及其与RPC、数据库、Redis的集成\n\n从这一章开始，我们正式进入微服务开发篇，共分上、中、下三篇。\n\n本章我们将讨论开发框架，框架与RPC、数据库、Redis的集成。\n\n2001年，我刚开始编程时，接触的第一个语言是\"ASP\"(没有.net)，它通过脚本注解的方式，实现动态功能(存取数据库等)，有点类似于PHP。在那个没有开发框架的年代，我们依然可以实现功能。但是这里只是“功能上的满足”，确无法做到“工程上的最优”，例如：\n\n- HTML与脚本混编，无论是页面样式修改，还是逻辑修改都很麻烦（视图、逻辑混合）\n\n- 有不少功能重复的代码，无法复用（如创建数据库连接）\n\n- 页面之间的内部依赖难以处理（往往只能通过url / session参数传递）\n\n开发框架的出现，解决了上述部分问题，以Spring为例：\n\n- Spring MVC实现的分层架构，将页面、视图、逻辑层强制分离\n\n- Spring JPA组件可以创建数据库模板，减少重复代码\n\n- 通过IoC容器，可以清晰地分离逻辑、处理依赖\n\n- ....\n\n当然，引入开发框架会带来额外的学习成本。Spring Boot借鉴了ROR框架中“约定优于配置”的设计理念，进行了大量的改造，实现了框架的“开箱可用”，有效降低了学习成本。\n\n本章会使用一个微服务为例，介绍Gradle + Spring Boot的基础集成。在此基础上，我们会介绍几个与框架紧密相关的内容：RPC框架、数据库、Redis的集成。\n"
  },
  {
    "path": "src/ch02-ms-dev1/database1.md",
    "content": "# Spring Boot集成SQL数据库1\n\n从银行的交易数据到打车订单，衣食住行，都离不开数据库的存储。\n\n在接下来的两个小节中，我们将通过3种不同的技术，在Spring Boot中集成MySQL数据库。\n\n- JDBC\n\n- MyBatis\n\n- JPA (Hibernate)\n\n本节的前半部分，我们将通过Docker快速搭建MySQL的环境，随后介绍JDBC的集成方式。\n\n## 搭建MySQL实验环境\n\n本书的重点是讨论微服务实战，我们直接使用Docker的方式，快速搭建实验环境。\n\n如果你想部署在生产环境，请参考官方[部署文档](https://dev.mysql.com/doc/mysql-installation-excerpt/8.0/en/linux-installation.html)。\n\n首先，请确认已经成功安装了Docker：\n\n```shell\ndocker ps \nCONTAINER ID   IMAGE       COMMAND                  CREATED        STATUS       PORTS                                                  NAMES\n```\n\n若尚未安装Docker，可以参考[官方文档]([Install Docker Engine | Docker Documentation](https://docs.docker.com/engine/install/))。\n\nMySQL的Docker运行脚本如下：\n\n```bash\n#!/bin/bash\n\nNAME=\"mysql\"\nPUID=\"1000\"\nPGID=\"1000\"\n\nVOLUME=\"$HOME/docker_data/mysql\"\nMYSQL_ROOT_PASS=\"123456\"\nmkdir -p $VOLUME \n\ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    --volume \"$VOLUME\":/var/lib/mysql \\\n    --env MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASS \\\n    --env PUID=$PUID \\\n    --env PGID=$PGID \\\n    -p 3306:3306 \\\n    --detach \\\n    --restart always \\\n    mysql:8.0\n```\n\n如脚本所述：\n\n- 使用官方的8.0镜像启动Docker\n\n- 退出后自动重启\n\n- 暴露3306端口到本机\n\n- 设置Volume盘到~/docker_data/mysql路径下\n\n- root密码123456(请务必更改为安全密码)\n\n执行后的效果：\n\n```bash\ndocker ps\nCONTAINER ID   IMAGE       COMMAND                  CREATED        STATUS       PORTS                                                  NAMES\nfeb2838197a6   mysql:8.0   \"docker-entrypoint.s…\"   46 hours ago   Up 7 hours   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   mysql\n```\n\n启动成功后，我们尝试连接数据库，新建库并授权给用户：\n\n```bash\nmysql -h 127.0.0.1 -u root -p\n\n> CREATE DATABASE homs_demo;\n> CREATE USER 'HomsDemo'@'%' identified by '123456';\n> GRANT ALL PRIVILEGES ON homs_demo.* TO 'HomsDemo'@'%';\n```\n\n尝试用新用户登录：\n\n```bash\nmysql -h 127.0.0.1 -u HomsDemo -p homs_demo\n```\n\n若能成功登录，我们创建本书实验所需的表：\n\n```sql\nCREATE TABLE `users` (\n  `id` bigint NOT NULL AUTO_INCREMENT,\n  `name` varchar(64) NOT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n```\n\n这里我们创建了表users，有两个列：id和name。\n\n温馨提示：我们使用utf8mb4字符集，如果用utf8是会有坑，可以参考[这篇文章]([掘金](https://adamhooper.medium.com/in-mysql-never-use-utf8-use-utf8mb4-11761243e434))。强烈推荐你对所有的数据表，都设置为utf8mb4。\n\n## Spring Boot 集成 JDBC操作MySQL\n\n我们先通过集成jdbc的方式操作MySQL数据库。\n\n首先在server项目的build.gradle中添加依赖\n\n```groovy\nimplementation 'org.springframework.boot:spring-boot-starter-jdbc'\nimplementation 'mysql:mysql-connector-java:8.0.20'\n```\n\n上述依赖中：\n\n- spring-boot-starter-jdbc是集成jdbc的starter依赖包\n\n- mysql-connector-java是集成MySQL的驱动\n\n接着，我们配置下数据源：\n\n```yaml\nspring.datasource:\n  url: jdbc:mysql://127.0.0.1:3306/homs_demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false\n  username: HomsDemo\n  password: 123456\n  hikari:\n    minimumIdle: 10\n    maximumPoolSize: 100\n```\n\n上述配置分为两部分：\n\n- spring.datasource.url / username / password定义了MySQL的访问链接\n\n- hikari是数据库连接池的配置。\n\nHikari是Spring Boot 2默认的链接池，[官方性能评测优秀](https://github.com/brettwooldridge/HikariCP-benchmark)。这里我们配置了minimumIdle(最小连接数)和maximumPoolSize(最大连接数)两个选项。更多配置参数可以参考[官方文档]([GitHub - brettwooldridge/HikariCP: 光 HikariCP・A solid, high-performance, JDBC connection pool at last.](https://github.com/brettwooldridge/HikariCP#gear-configuration-knobs-baby))。\n\n经过上述的组合配置后，对应DataSource对应的Configuration会自动激活，并注册一系列的关联Bean。\n\n下面让我们使用它访问MySQL数据库：\n\n```java\n@Repository\npublic class UserRepository1Impl implements UserRepository {\n\n    @Autowired\n    protected NamedParameterJdbcTemplate db;\n\n    private static RowMapper<User> ROW_MAPPER = new BeanPropertyRowMapper<>(User.class);\n\n    @Override\n    public Optional<Long> create(User user) {\n        String sql = \"INSERT INTO `users`(`name`) VALUES(:name)\";\n        SqlParameterSource param = new MapSqlParameterSource(\"name\", user.getName());\n        KeyHolder holder = new GeneratedKeyHolder();\n        if (db.update(sql, param, holder) > 0) {\n            return Optional.ofNullable(holder.getKey().longValue());\n        } else {\n            return Optional.empty();\n        }\n    }\n\n    @Override\n    public Optional<User> getUser(long id) {\n        String sql = \"SELECT * FROM `users` WHERE `id` = :id\";\n        SqlParameterSource param = new MapSqlParameterSource(\"id\", id);\n        try {\n            return Optional.ofNullable(db.queryForObject(sql, param, ROW_MAPPER));\n        } catch (EmptyResultDataAccessException e) {\n            return Optional.empty();\n        }\n    }\n\n    @Override\n    public Optional<User> getUserByName(String name) {\n        String sql = \"SELECT * FROM `users` WHERE `name` = :name\";\n        SqlParameterSource param = new MapSqlParameterSource(\"name\", name);\n        try {\n            return Optional.ofNullable(db.queryForObject(sql, param, ROW_MAPPER));\n        } catch (EmptyResultDataAccessException e) {\n            return Optional.empty();\n        }\n    }\n}\n```\n\n在上面的代码中，我们自动装配了\"NamedParameterJdbcTemplate\"，然后用它访问MySQL数据库：\n\n- 读请求使用db.query，配合RowMapper做类型转化\n\n- 写请求使用db.update，配合KeyHolder获取自增主键\n\n使用JDBC访问MySQL的方式，优点和缺点是完全一样的：使用显示的SQL语句操作数据库。\n\n优点：直接、方便代码Review和性能检查\n\n缺点：SQL编写过程繁琐、易错，特别是对于CRUD请求，效率较低\n"
  },
  {
    "path": "src/ch02-ms-dev1/database2.md",
    "content": "# Spring Boot集成SQL数据库2\n\n## Spring Boot 集成 MyBatis操作MySQL\n\nMyBatis是一款半自动的ORM框架。由于某国内大厂的广泛使用，MyBatis在国内非常火热(在国外其热度不如Hibernate)。\n\n首先还是集成依赖：\n\n```groovy\nimplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'\nimplementation 'mysql:mysql-connector-java:8.0.20'\n```\n\n套路与jdbc类似，但starter并不是官方的了，而是mybatis自己做的starter，感兴趣的可以来[这里](https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter/2.2.0)看下具体组成(会有惊喜)。\n\n接下来是yaml配置环节：\n\n```yaml\nspring.datasource:\n  url: jdbc:mysql://127.0.0.1:3306/homs_demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false\n  username: HomsDemo\n  password: 123456\n  hikari:\n    minimumIdle: 10\n    maximumPoolSize: 100\n\n# mybatis extra\nmybatis:\n  configuration:\n    map-underscore-to-camel-case: true\n  type-aliases-package: com.coder4.homs.demo.server.mybatis.dataobject\n```\n\n不难发现，数据库链接的定义复用了jdbc的那一套，MyBatis的定义分3行，如下：\n\n- configuration：开启驼峰规则转化\n\n- type-aliases-package：mapper文件存放的包名\n\n更多MyBatis的配置选项可以参考[这里]([mybatis-spring-boot-autoconfigure – Introduction](https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/))\n\n接着，我们定义Mapper，在MyBatis中，Mapper相当于前面手写的Repository，定义如下：\n\n```java\npackage com.coder4.homs.demo.server.mybatis.mapper;\n\nimport com.coder4.homs.demo.server.mybatis.dataobject.UserDO;\nimport org.apache.ibatis.annotations.Mapper;\nimport org.apache.ibatis.annotations.Param;\nimport org.springframework.stereotype.Repository;\n\n/**\n * <p>\n *  Mapper 接口\n * </p>\n *\n * @author author\n * @since 2021-09-09\n */\n@Repository\n@Mapper\npublic interface UserMapper {\n\n    @Insert(\"INSERT INTO users(name) VALUES(#{name})\")\n    @Options(useGeneratedKeys = true, keyProperty = \"id\")\n    long create(UserDO user);\n\n    @Select(\"SELECT * FROM users WHERE id = #{id}\")\n    UserDO getUser(@Param(\"id\") Long id);\n\n    @Select(\"SELECT * FROM users WHERE name = #{name}\")\n    UserDO getUserByName(@Param(\"name\") String name);\n\n}\n```\n\n你可能会奇怪：这不是接口(interface)么，并没有实现？\n\n是的，通过定义@Repository和@Mapper，MyBatis会通过运行时的切面注入，帮我们自动实现，具体执行的SQL和映射，会读取@Select、@Options等注解中的配置。\n\n经过上述介绍，你可以发现：\n\nMyBatis可以直接通过注解的方式快速访问数据库，(相对于JDBC的)精简了大量无用代码。\n\n同时，MyBatis依然需要指定运行的SQL语句，这与JDBC的方式是一致的。虽然有些繁琐，但可以保证性能可控。\n\n如果你在网上搜索\"MyBatis Spring集成\"，会找到大量xml配置的用法。\n\n在一些老项目中，xml是标准的集成方式。在这种配置方式下，配置繁琐、代码量大，即使借助\"MyBatisX\"等插件，也依然较为复杂。\n\n因此，除非你要维护遗留的老项目代码，我都建议你使用(本文中)注解式集成MyBatis。\n\n## Spring Boot集成 JPA 操作MySQL\n\nJPA的全称是Java Persistence API，即持久化访问规范API。\n\nSpring也提供了集成JPA的方案，称为 Spring Data JPA，其底层是通过Hibernate的JPA来实现的。\n\n首先集成依赖：\n\n```groovy\nimplementation 'org.springframework.boot:spring-boot-starter-data-jpa'\nimplementation 'mysql:mysql-connector-java:8.0.20'\n```\n\n与前面类似，不再重复介绍。\n\n接着是配置：\n\n```yaml\n# jdbc demo\nspring.datasource:\n  url: jdbc:mysql://127.0.0.1:3306/homs_demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false\n  username: HomsDemo\n  password: 123456\n  hikari:\n    minimumIdle: 10\n    maximumPoolSize: 100\n\n\n# jpa demo\nspring.jpa:\n  database-platform: org.hibernate.dialect.MySQL8Dialect\n  hibernate.ddl-auto: validate\n```\n\n在MySQL连接上，我们依然复用了Spring DataSource的配置。\n\njpa侧的配置为：\n\n- database-platform：设置使用MySQL8语法\n\n- hibernate.ddl-auto：只校验表，不回主动更新数据表的结构\n\n接着，我们来定义实体(Entity)：\n\n```java\n@Entity\n@Data\n@Table(name = \"users\")\npublic class UserEntity {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private long id;\n\n    // @Column(name = \"name\")\n    private String name;\n\n    public User toUser() {\n        User user = new User();\n        user.setId(id);\n        user.setName(name);\n        return user;\n    }\n\n}\n```\n\n这里我们将UserEntity与表\"users\"做了关联。\n\n接下来是Repository：\n\n```java\n@Repository\npublic interface UserJPARepository extends CrudRepository<UserEntity, Long> {\n\n    Collection<UserEntity> findByName(String name);\n\n}\n```\n\n我们继承了CrudRepository，他会自动生成针对UserEntity的CRUD操作。\n\n此外，我们还定义了1个额外函数：\n\n- findByName，通过隐士语法规则，让JPA自动帮我们生成对应SQL\n\n从直观感受上，JPA比MyBatis更加“高级” -- 一些简单的SQL都不用写了。\n\n但天下真的有免费的馅饼么？我们先卖个关子。\n\n## JMJ应该选哪个\n\n经过这两节的介绍，你已经掌握了JDBC、MyBatis、JPA三种操作数据库的方式。\n\n在实战中，究竟要选哪个呢？\n\n从易用性的角度来评估，我们可以得出结论：JPA > MyBatis > JDBC\n\n那么从性能的角度来看呢？\n\n我们使用wrk做了(get-by-id接口的)简单压测，结论如下：\n\n|         | 读QPS |\n| ------- | ---- |\n| JDBC    | 457  |\n| MyBatis | 445  |\n| JPA     | 114  |\n\n这里，你会惊讶的发现：\n\n- JDBC和MyBatis的性能差别不大，在5%以内\n\n- JPA(Hibernate)的性能，居然只有其余两种方式的1/3 \n\n如此差的性能，真的让人百思不得其解，我尝试打印了SQL和执行耗时，并没有发现什么异常。\n\n更进一步的，我们尝试用指定SQL的方式，替换了自动生成的接口，如下\n\n```java\n@Repository\npublic interface UserJPARepository extends CrudRepository<UserEntity, Long> {\n\n    @Query(value = \"SELECT * FROM users WHERE id = :id\", nativeQuery = true)\n    Optional<UserEntity> findByIdFast(@Param(\"id\") long id);\n\n}\n```\n\n这次的压测结果是：447，性能基本和JDBC持平了。但是这种NativeSQL的用法并没有使用自动生成SQL的功能，没有发挥Hibernate本来的功效。\n\n所以，我们认为，锅在于Hibernate自动生成SQL的逻辑耗时过大。\n\n当然，Hibernate也不是一无是处，针对多层关联，建模复杂的场景，使用Entity做映射，会更加方便。\n\n让我们回到前面的问题上：JMJ应该选哪个？\n\n- 如果对性能有极致要求，建议JDBC或者MyBatis。\n\n- 如果建模场景复杂，嵌套密集，且对性能要求不高，可以选用Hibernate。\n"
  },
  {
    "path": "src/ch02-ms-dev1/gradle.md",
    "content": "# Gradle构建工具配置\n\n构建工具解决了依赖管理、打包流程、项目结构工程化等问题，是现代软件开发中的必备工具。\n\nGradle是一款Java开发语言的构建工具，兼容POM以来，使用Groovy作为描述语言，构建速度快、可拓展性强，是大量项目的首选。\n\n在本节中，我们将介绍Gradle的基本用法与配置。\n\n## Gradle的下载与安装\n\n我们使用稳定版7.2，你可以在[官网](https://gradle.org/releases/)下载二进制版本。\n\n解压缩后，需要将二进制目录加入你的PATH路径：\n\n```shell\nexport PATH=$PATH:HOME/soft/gradle/bin/\n```\n\n然后执行gradle，查看是否安装成功\n\n```shell\ngradle -v\n\n------------------------------------------------------------\nGradle 7.2\n------------------------------------------------------------\n\nBuild time:   2021-08-17 09:59:03 UTC\nRevision:     a773786b58bb28710e3dc96c4d1a7063628952ad\n\nKotlin:       1.5.21\nGroovy:       3.0.8\nAnt:          Apache Ant(TM) version 1.10.9 compiled on September 27 2020\nJVM:          1.8.0_291 (Oracle Corporation 25.291-b10)\nOS:           Mac OS X 10.16 x86_64\n```\n\n## 修改Gradle的Maven仓库镜像\n\ngradle的依赖使用了Maven的仓库。由于众所周知的原因，这些仓库在国内的速度并不稳定，我们需要将仓库切换成国内镜像。\n\n修改~/.gradle/init.gradle文件如下：\n\n```\n// project\nallprojects{\n    repositories {\n    mavenLocal()\n        maven { url 'https://maven.aliyun.com/repository/public/' }\n        maven { url 'https://maven.aliyun.com/repository/jcenter/' }\n        maven { url 'https://maven.aliyun.com/repository/google/' }\n        maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }\n        maven { url 'https://jitpack.io/' }\n    }\n}\n\n// plugin\nsettingsEvaluated { settings ->\n    settings.pluginManagement {\n\n        // Clear repositories collection\n        repositories.clear()\n\n        // Add my Artifactory mirror\n        repositories {\n            mavenLocal()\n            maven {\n                url \"https://maven.aliyun.com/repository/gradle-plugin/\"\n            }\n        }\n    }\n}\n```\n\n解释下文件配置：\n\n- 上半部分：将maven中央仓库、jcenter仓库都修改为国内镜像(阿里云)，并增加了jitpack仓库(后续章节会使用)。\n\n- 下半部分：将gradle插件仓库修改为国内镜像，这部分是必须的，不要忘记。\n\n我们可以通过一个简单的脚本，检查配置是否生效\n\n验证脚本build.gradle\n\n```groovy\ntask listrepos {\n    doLast {\n        println \"Repositories:\"\n        project.repositories.each { println \"Name: \" + it.name + \"; url: \" + it.url }\n   }\n}\n```\n\n执行验证：\n\n```\ngradle listrepos\n\nRepositories:\nName: MavenLocal; url: file:/Users/coder4/.m2/repository/\nName: maven; url: https://maven.aliyun.com/repository/public/\nName: maven2; url: https://maven.aliyun.com/repository/jcenter/\nName: maven3; url: https://maven.aliyun.com/repository/google/\nName: maven4; url: https://maven.aliyun.com/repository/gradle-plugin/\nName: maven5; url: https://jitpack.io/\nIntelliJ\n```\n\n## gradle-wrapper生成\n\ngradle-wrapper是用于执行gradle的脚本 + 精简版的gradle二进制文件。\n\n既然已经有了gradle，为什么还要单独弄一个wrapper出来么？\n\n- 方便没有安装gradle的环境执行构建(例如打包机)\n\n- 支持多版本gradle的快速切换(实现nvm的效果)\n\n初始化gradle项目时，执行如下命令：\n\n```shell\ngradle init\n```\n\ngradle会生成如下wrapper相关文件：\n\n```shell\n├── gradle\n│   └── wrapper\n│       ├── gradle-wrapper.jar\n│       └── gradle-wrapper.properties\n├── gradlew\n├── gradlew.bat\n└── settings.gradle\n```\n\n建议将上述文件一并加入git仓库中，以防出现版本兼容问题。\n\n## IntelliJ IDEA中配置Gradle\n\nIntelliJ IDEA是一款功能强大的IDE，是许多Java程序员的首选。\n\nIDEA默认支持Gradle，请确保配置正确：\n\n![  ](./gradle-idea.png)\n\n上方的Gradle配置文件默认路径，请维持默认配置，使用家目录下默认的。\n\n下方的Gradle版本，推荐使用默认选项(gradle-wrapper.properties)，即使用项目路径下gradle-wrapper.properties指定的版本。\n\n经过上述配置，我们已经搭建了Gradle的构建环境。在下一节，我们会在此基础上集成Spring Boot框架。\n"
  },
  {
    "path": "src/ch02-ms-dev1/redis.md",
    "content": "# Spring Boot集成Redis内存数据库\n\n常规的业务数据，一般选择存储在SQL数据库中。\n\n传统的SQL数据库基于磁盘存储，可以正常的流量需求。然而，在高并发应用场景中容易被拖垮，导致系统崩溃。\n\n针对这种情况，我们可以通过增加缓存、使用NoSQL数据库等方式进行优化。\n\nRedis是一款开源的内存NoSQL数据库，其稳定性高、[性能强悍]([How fast is Redis? – Redis](https://redis.io/topics/benchmarks))，是KV细分领域的[市场占有率冠军](https://db-engines.com/en/ranking/key-value+store)。\n\n本节将介绍Redis与Spring Boot的集成方式。\n\n## Redis环境准备\n\n与前文类似，我们使用Docker快速部署Redis服务器。\n\n```bash\n#!/bin/bash\n\nNAME=\"redis\"\nPUID=\"1000\"\nPGID=\"1000\"\n\nVOLUME=\"$HOME/docker_data/redis\"\nmkdir -p $VOLUME \n\ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    --volume \"$VOLUME\":/data \\\n    -p 6379:6379 \\\n    --detach \\\n    --restart always \\\n    redis:6 \\\n    redis-server --appendonly yes --requirepass redisdemo\n```\n\n在上述脚本中：\n\n- 使用了最新的redis 6镜像\n\n- 开启\"appendonly\"的持久化方式\n\n- 启用密码\"redisdemo\"\n\n- 端口暴露为6379\n\n我们尝试连接一下：\n\n```bash\nredis-cli -h 127.0.0.1 -a redisdemo\n```\n\n成功！(如果你没有redis-cli的可执行文件，可以到[官网下载](https://redis.io/download))\n\n## Redis的缓存使用\n\nSpring提供了内置的Cache框架，可以通过@Cache注解，轻松实现redis Cache的功能。\n\n首先引入依赖：\n\n```groovy\nimplementation 'org.springframework.boot:spring-boot-starter-data-redis'\nimplementation 'org.springframework.boot:spring-boot-starter-json'\nimplementation 'org.apache.commons:commons-pool2:2.11.0'\n```\n\n上述依赖的作用分别为：\n\n- redis客户端：Spring Boot 2使用的是[lettuce](http://github.com/lettuce-io/lettuce-core)\n\n- json依赖：我们要使用jackson做json的序列化 / 反序列化\n\n- commons-pool2线程池，这里其实是data-redis没处理好，需要额外加入，按理说应该集成在starter里的\n\n接着我们在application.yaml中定义数据源：\n\n```yaml\n# redis demo\nspring:\n  redis:\n    host: 127.0.0.1\n    port: 6379\n    password: \"redisdemo\"\n    lettuce:\n      pool:\n        max-active: 50\n        min-idle: 5\n```\n\n接着我们需要设置自定义的Configuration：\n\n```java\npackage com.coder4.homs.demo.server.configuration;\n\nimport com.fasterxml.jackson.annotation.JsonAutoDetect;\nimport com.fasterxml.jackson.annotation.PropertyAccessor;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;\nimport com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;\nimport org.springframework.cache.annotation.CachingConfigurerSupport;\nimport org.springframework.cache.annotation.EnableCaching;\nimport org.springframework.cache.interceptor.KeyGenerator;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.data.redis.cache.RedisCacheConfiguration;\nimport org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;\nimport org.springframework.data.redis.serializer.RedisSerializationContext;\n\nimport java.time.Duration;\n\n/**\n * @author coder4\n */\n@Configuration\n@EnableCaching\npublic class RedisCacheCustomConfiguration extends CachingConfigurerSupport {\n\n    @Bean\n    public KeyGenerator keyGenerator() {\n        return (target, method, params) -> {\n            StringBuilder sb = new StringBuilder();\n            // sb.append(target.getClass().getName());\n            sb.append(target.getClass().getSimpleName());\n            sb.append(\":\");\n            sb.append(method.getName());\n            for (Object obj : params) {\n                sb.append(obj.toString());\n                sb.append(\":\");\n            }\n            sb.deleteCharAt(sb.length() - 1);\n            return sb.toString();\n        };\n    }\n\n    @Bean\n    public RedisCacheConfiguration redisCacheConfiguration() {\n        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>(Object.class);\n        ObjectMapper objectMapper = new ObjectMapper();\n        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);\n        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, DefaultTyping.NON_FINAL);\n        // use json serde\n        serializer.setObjectMapper(objectMapper);\n        return RedisCacheConfiguration.defaultCacheConfig()\n                .entryTtl(Duration.ofMinutes(5)) // 5 mins ttl\n                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer));\n    }\n\n}\n```\n\n上述主要包含两部分：\n\n- KeyGenerator可以根据Class + method + 参数 生成唯一的key名字，用于Redis中存储的key\n\n- RedisCacheConfiguration做了2处定制：\n  \n  - 更改了序列化方式，从默认的Java(Serilization更改为Jackson(json)\n  \n  - 缓存过期时间为5分钟\n\n接着，我们在项目中使用Cache\n\n```java\npublic interface UserRepository {\n\n    Optional<Long> create(User user);\n\n    @Cacheable(value = \"cache\")\n    Optional<User> getUser(long id);\n\n    Optional<User> getUserByName(String name);\n}\n```\n\n这里我们用了@Cache注解，\"cache\"是key的前缀\n\n访问一下：\n\n```bash\ncurl http://127.0.0.1:8080/users/1\n```\n\n然后看一下redis\n\n```bash\nredis-cli -a redisdemo\n\n> keys *\n> \"cache::UserRepository1Impl:getUser1\"\n> get \"cache::UserRepository1Impl:getUser1\"\n\"[\\\"com.coder4.homs.demo.server.model.User\\\",{\\\"id\\\":1,\\\"name\\\":\\\"user1\\\"}]\"\n> ttl \"cache::UserRepository1Impl:getUser1\"\n> 293\n```\n\n数据被成功缓存在了Redis中(序列化为json)，并且会自动过期。\n\n我们使用[Spring Boot集成SQL数据库2](./database2.md)一节中的压测脚本验证性能，QPS达到860，提升达80%。\n\n在数据发生删除、更新时，你需要更新缓存，以确保一致性。推荐你阅读[缓存更新的套路]([缓存更新的套路 | 酷 壳 - CoolShell](https://coolshell.cn/articles/17416.html))。\n\n在更新/删除方法上应用@CacheEvict(beforeInvocation=false)，可以实现更新时删除的功能。\n\n## Redis的持久化使用\n\nRedis不仅可以用作缓存，也可以用作持久化的存储。\n\n首先请确认Redis已开启持久化：\n\n```\n127.0.0.1:6379> config get save\n1) \"save\"\n2) \"3600 1 300 100 60 10000\"\n\n127.0.0.1:6379> config get appendonly\n1) \"appendonly\"\n2) \"yes\"\n```\n\n上述分别为rdb和aof的配置，有任意一个非空，即表示开启了持久化。\n\n实际上，在我们集成Spring Data的时候，会自动配置RedisTemplte，使用它即可完成Redis的持久化读取。\n\n不过默认配置的Template有一些缺点，我们需要做一些改造：\n\n```java\npackage com.coder4.homs.demo.server.configuration;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.data.redis.core.RedisTemplate;\nimport org.springframework.data.redis.serializer.RedisSerializer;\nimport org.springframework.data.redis.serializer.StringRedisSerializer;\n\n/**\n * @author coder4\n */\n@Configuration\npublic class RedisTemplateConfiguration {\n\n    @Autowired\n    public void decorateRedisTemplate(RedisTemplate redisTemplate) {\n        RedisSerializer stringSerializer = new StringRedisSerializer();\n        redisTemplate.setKeySerializer(stringSerializer);\n        redisTemplate.setKeySerializer(stringSerializer);\n        redisTemplate.setValueSerializer(stringSerializer);\n        redisTemplate.setHashKeySerializer(stringSerializer);\n        redisTemplate.setHashValueSerializer(stringSerializer);\n    }\n\n}\n```\n\n如上所述，我们设置RedisTemplate的KV，分别采用String的序列化方式。\n\n接着我们在代码中使用其存取Redis：\n\n```java\n@Autowired\nprivate RedisTemplate redisTemplate;\n\n\nredisTemplate.boundValueOps(\"key\").set(\"value\");\n```\n\nRedisTemplate的语法稍微有些奇怪，你也可以直接使用Conn来做操作，这样更加\"Lettuce\"。\n\n```java\n@Autowired\nprivate LettuceConnectionFactory leconnFactory;\n\ntry (RedisConnection conn = leconnFactory.getConnection()) {\n    conn.set(\"hehe\".getBytes(), \"haha\".getBytes());\n}\n```\n\n至此，我们已经完成了Spring Boot 与 Redis的集成。\n\n思考题：当一个微服务需要连接多组Redis，该如何集成呢？\n\n请自己探索，并验证其正确性。\n"
  },
  {
    "path": "src/ch02-ms-dev1/rpc.md",
    "content": "# Spring Boot集成gRPC框架\n\ngRPC是谷歌开源的高性能、开源、通用RPC框架。由于gRPC基于HTTP2协议，所以其对移动端非常友好。\n\n本节将介绍Spring Boot集成gRPC的服务端、客户端。\n\n### 安装protoc及gRPC\n\ngRPC默认使用[Protocol Buffers]([Protocol Buffers &nbsp;|&nbsp; Google Developers](https://developers.google.com/protocol-buffers))做为序列化协议，我们首先安装protoc编译器：\n\n在这里下载最新版本的[protoc](https://github.com/protocolbuffers/protobuf/releases/tag/v3.17.3)编译器，请根据你的操作系统选择对应版本，这里我选用MacOSX的。\n\n```bash\nwget https://github.com/protocolbuffers/protobuf/releases/download/v3.17.3/protoc-3.17.3-osx-x86_64.zip\nunzip protoc-3.17.3-osx-x86_64.zip\n```\n\n解压缩后，将其加入PATH路径下：\n\n```bash\nexport PATH=$PATH:$YOUR_PROTOC_PATH\n```\n\n试一下是能否执行：\n\n```bash\nprotoc --version\nlibprotoc 3.17.3\n```\n\n除此之外，我们还需要一个gRPC的Java插件，才能生成gRPC的桩代码，你可以在[这里]([Maven Central Repository Search](https://search.maven.org/search?q=a:protoc-gen-grpc-java))找到最新版本。这里我们依然选择OSX的64位版本：\n\n```bash\nwget https://search.maven.org/remotecontent?filepath=io/grpc/protoc-gen-grpc-java/1.40.1/protoc-gen-grpc-java-1.40.1-osx-x86_64.exe\n```\n\n下载后，将其加入PATH路径中。尝试定位一下：\n\n```bash\nwhich protoc-gen-grpc-java \nYour_Path/protoc-gen-grpc-java\n```\n\n至此，protoc和grpc的安装准备工作已经就绪。\n\n## Client侧集成\n\n首先是集成依赖，我们放在client子项目的builld.gradle中：\n\n```groovy\nimplementation 'com.google.protobuf:protobuf-java:3.17.3'\nimplementation \"io.grpc:grpc-stub:1.39.0\"\nimplementation \"io.grpc:grpc-protobuf:1.39.0\"\nimplementation 'io.grpc:grpc-netty-shaded:1.39.0'\n```\n\n由于版本依赖较多，我建议使用platform统一管理，可以参考[前文](./spring-boot.md)。\n\n接着，我们编写protoc文件，HomsDemo.proto：\n\n```protobuf\nsyntax = \"proto3\";\noption java_package = \"com.coder4.homs.demo\";\noption java_outer_classname = \"HomsDemoProto\";\n;\n\nmessage AddRequest {\n    int32 val1 = 1;\n    int32 val2 = 2;\n}\n\nmessage AddResponse {\n    int32 val = 1;\n}\n\nmessage AddSingleRequest {\n    int32 val = 1;\n}\n\nservice HomsDemo {\n    rpc Add(AddRequest) returns (AddResponse);\n    rpc Add2(stream AddSingleRequest) returns (AddResponse);\n}\n```\n\n我们添加了两个RPC方法：\n\n- Add是正常的调用\n\n- Add2是单向Stream调用\n\n接着，我们需要编译，生成桩文件：\n\n```bash\n#!/bin/sh\n\nDIR=`cd \\`dirname ${BASH_SOURCE[0]}\\`/.. && pwd`\n\nprotoc HomsDemo.proto --java_out=${DIR}/homs-demo-client/src/main/java --proto_path=${DIR}/homs-demo-client/src/main/java/com/coder4/homs/demo/\nprotoc HomsDemo.proto --plugin=protoc-gen-grpc-java=`which protoc-gen-grpc-java` --grpc-java_out=${DIR}/homs-demo-client/src/main/java --proto_path=${DIR}/homs-demo-client/src/main/java/com/coder4/homs/demo/\n```\n\n这里分为两个步骤：\n\n- 第一次protoc编译，生成protoc的桩文件\n\n- 第二次protoc编译，使用了protoc-gen-grpc-java的插件，生成gRPC的服务端和客户端文件\n\n编译成功后，路径如下：\n\n```bash\nhoms-demo-client\n\n├── build.gradle\n└── src\n    └── main\n        └── java\n            └── com\n                └── coder4\n                    └── homs\n                        └── demo\n                            ├── HomsDemo.proto\n                            ├── HomsDemoGrpc.java\n                            └── HomsDemoProto.java\n```\n\n如上所示：HomsDemoProto是protoc的桩文件，HomsDemoGrpc是gRPC服务的桩文件。\n\n下面我们来编写客户端代码，HomsDemoClient.java：\n\n```java\npackage com.coder4.homs.demo.client;\n\nimport com.coder4.homs.demo.HomsDemoGrpc;\n\nimport com.coder4.homs.demo.HomsDemoProto.AddRequest;\nimport com.coder4.homs.demo.HomsDemoProto.AddResponse;\nimport com.coder4.homs.demo.HomsDemoProto.AddSingleRequest;\nimport io.grpc.Channel;\nimport io.grpc.ManagedChannel;\nimport io.grpc.ManagedChannelBuilder;\nimport io.grpc.StatusRuntimeException;\nimport io.grpc.stub.StreamObserver;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Optional;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicLong;\n\n\n/**\n * @author coder4\n */\npublic class HomsDemoClient {\n\n    private Logger LOG = LoggerFactory.getLogger(HomsDemoClient.class);\n\n    private final HomsDemoGrpc.HomsDemoBlockingStub blockingStub;\n\n    private final HomsDemoGrpc.HomsDemoStub stub;\n\n    /**\n     * Construct client for accessing HelloWorld server using the existing channel.\n     */\n    public HomsDemoClient(Channel channel) {\n        blockingStub = HomsDemoGrpc.newBlockingStub(channel);\n        stub = HomsDemoGrpc.newStub(channel);\n    }\n\n    public Optional<Integer> add(int val1, int val2) {\n        AddRequest request = AddRequest.newBuilder().setVal1(val1).setVal2(val2).build();\n        AddResponse response;\n        try {\n            response = blockingStub.add(request);\n            return Optional.ofNullable(response.getVal());\n        } catch (StatusRuntimeException e) {\n            LOG.error(\"RPC failed: {0}\", e.getStatus());\n            return Optional.empty();\n        }\n    }\n\n    public Optional<Integer> add2(Collection<Integer> vals) {\n\n        try {\n\n            CountDownLatch cdl = new CountDownLatch(1);\n\n            AtomicLong respVal = new AtomicLong();\n\n            StreamObserver<AddSingleRequest> requestStreamObserver =\n                    stub.add2(new StreamObserver<AddResponse>() {\n                        @Override\n                        public void onNext(AddResponse value) {\n                            respVal.set(value.getVal());\n                        }\n\n                        @Override\n                        public void onError(Throwable t) {\n                            cdl.countDown();\n                        }\n\n                        @Override\n                        public void onCompleted() {\n                            cdl.countDown();\n                        }\n                    });\n\n            for (int val : vals) {\n                requestStreamObserver.onNext(AddSingleRequest.newBuilder().setVal(val).build());\n            }\n            requestStreamObserver.onCompleted();\n\n            try {\n                cdl.await(1, TimeUnit.SECONDS);\n            } catch (InterruptedException e) {\n                e.printStackTrace();\n            }\n\n            return Optional.ofNullable(respVal.intValue());\n        } catch (StatusRuntimeException e) {\n            LOG.error(\"RPC failed: {0}\", e.getStatus());\n            return Optional.empty();\n        }\n    }\n}\n```\n\n代码如上所示：Add还是相对简单的，但是使用了Stream的Add2就比较复杂了。\n\n在上述代码中，需要传入Channel做为连接句柄，在假设知道IP和端口的情况下，可以如下构造：\n\n```java\nString target = \"127.0.0.1:5000\";\n        ManagedChannel channel = null;\n        try {\n            channel = ManagedChannelBuilder\n                    .forTarget(target)\n                    .usePlaintext()\n                    .build();\n        } catch (Exception e) {\n            LOG.error(\"open channel excepiton\", e);\n            return;\n        }\n\n\n        HomsDemoClient client = new HomsDemoClient(channel);\n```\n\n在微服务架构下，实例众多，获取每个IP显得不太实际，我们会在后续章节介绍集成服务发现的Channel构造方案。\n\n## Server侧集成\n\n老套路，首先是依赖集成：\n\n```groovy\nimplementation 'com.google.protobuf:protobuf-java:3.17.3'\nimplementation \"io.grpc:grpc-stub:1.39.0\"\nimplementation \"io.grpc:grpc-protobuf:1.39.0\"\nimplementation 'io.grpc:grpc-netty-shaded:1.39.0'\n```\n\n与上述客户端的集成完全一致。\n\n接下来我们实现RPC的服务逻辑：\n\n```java\n/**\n * @(#)HomsDemoImpl.java, 8月 12, 2021.\n * <p>\n * Copyright 2021 coder4.com. All rights reserved.\n * CODER4.COM PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.\n */\npackage com.coder4.homs.demo.server.grpc;\n\nimport com.coder4.homs.demo.HomsDemoGrpc.HomsDemoImplBase;\nimport com.coder4.homs.demo.HomsDemoProto.AddRequest;\nimport com.coder4.homs.demo.HomsDemoProto.AddResponse;\nimport com.coder4.homs.demo.HomsDemoProto.AddSingleRequest;\nimport io.grpc.stub.StreamObserver;\n\n/**\n * @author coder4\n */\npublic final class HomsDemoGrpcImpl extends HomsDemoImplBase {\n\n    @Override\n    public void add(AddRequest request, StreamObserver<AddResponse> responseObserver) {\n        responseObserver.onNext(AddResponse.newBuilder()\n                .setVal(request.getVal1() + request.getVal2())\n                .build());\n        responseObserver.onCompleted();\n    }\n\n    @Override\n    public StreamObserver<AddSingleRequest> add2(StreamObserver<AddResponse> responseObserver) {\n        return new StreamObserver<AddSingleRequest>() {\n\n            int sum = 0;\n\n            @Override\n            public void onNext(AddSingleRequest value) {\n                sum += value.getVal();\n            }\n\n            @Override\n            public void onError(Throwable t) {\n\n            }\n\n            @Override\n            public void onCompleted() {\n                responseObserver.onNext(AddResponse.newBuilder()\n                        .setVal(sum)\n                        .build());\n                sum = 0;\n                responseObserver.onCompleted();\n            }\n        };\n    }\n}\n```\n\n这里要特别说明，因为gRPC都是异步回调的方式，所以其RPC在实现上有点反直觉：\n\n- 通过responseObserver.onNext返回调用结果\n\n- 通过responseObserver.onCompleted结束调用\n\n而add2方法，由于采用了Client-Streaming，所以实现会更加复杂一些。\n\n实际上，gRPC支持[4种调用模式]([Generated-code reference | Java | gRPC](https://grpc.io/docs/languages/java/generated-code/))：\n\n- Unary: 客户端单输入，服务端单输出\n\n- Client-Streaming: 客户端多输入，服务端单输出\n\n- Server-Streaming: 客户端单输入，服务端多输出\n\n- Bidirectional-Streaming: 客户端多输入，服务端多输出\n\n由于篇幅所限，本文种只实现了前2种，推荐你手动实现另外的两种模式。\n"
  },
  {
    "path": "src/ch02-ms-dev1/spring-boot.md",
    "content": "# Sprint Boot项目与Gradle的集成\n\n本节我们将借助Spring Start快速搭建微服务项目。\n\n在此基础上，我们会将工程改造成子项目的组织形式。\n\n## Spring Start快速生成项目\n\n为了降低微服务的开发门槛，社区提供了[Spring initializr](https://start.spring.io/)工具。它可以一键生成微服务项目。如图所示：\n\n![f](./spring-start.png)\n\n我们需要注意几个配置：\n\n- Project(项目)：选择Gradle\n\n- Language(开发语言)：选择Java\n\n- Spring Boot(版本)：选择2.5.4\n\n- 下面的工程名、包名根据自己的需要填写\n\n- Java(版本)：选择8\n\n完成后，点击下方的GENERATE(生成)按钮，即可下载项目的zip包。\n\n解压缩后，目录结构如下：\n\n```shell\n.\n├── HELP.md\n├── build.gradle\n├── gradle\n│   └── wrapper\n│       ├── gradle-wrapper.jar\n│       └── gradle-wrapper.properties\n├── gradlew\n├── gradlew.bat\n├── settings.gradle\n└── src\n    ├── main\n    │   ├── java\n    │   │   └── com\n    │   │       └── coder4\n    │   │           └── homsdemo\n    │   │               └── HomsDemoApplication.java\n    │   └── resources\n    │       ├── application.properties\n    │       ├── static\n    │       └── templates\n    └── test\n        └── java\n            └── com\n                └── coder4\n                    └── homsdemo\n                        └── HomsDemoApplicationTests.java\n```\n\n这是一个标准的gradle项目路径：\n\n- gradle*：gradle相关文件，可以参考[Gradle构建工具配置](./gradle.md)一节中的介绍\n- src：项目源文件\n- test：项目单元测试文件\n\n我们来看一下src目录下唯一的Java源文件，HomsDemoApplication.java：\n\n```\npackage com.coder4.homsdemo;\n\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n@SpringBootApplication\npublic class HomsDemoApplication {\n\n    public static void main(String[] args) {\n        SpringApplication.run(HomsDemoApplication.class, args);\n    }\n\n}\n```\n\n借助Spring Boot的精简设计，项目只需上述一个源文件即可服务端进程\n\n编译项目：\n\n```shell\ngradle build\n\nBUILD SUCCESSFUL in 19s\n7 actionable tasks: 7 executed\n```\n\n运行项目：\n\n```\njava -jar ./build/libs/homs-demo-0.0.1-SNAPSHOT.jar \n\n  .   ____          _            __ _ _\n /\\\\ / ___'_ __ _ _(_)_ __  __ _ \\ \\ \\ \\\n( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\\n \\\\/  ___)| |_)| | | | | || (_| |  ) ) ) )\n  '  |____| .__|_| |_|_| |_\\__, | / / / /\n =========|_|==============|___/=/_/_/_/\n :: Spring Boot ::                (v2.5.4)\n\n2021-09-08 12:47:51.906  INFO 2806 --- [           main] com.coder4.homsdemo.HomsDemoApplication  : Starting HomsDemoApplication using Java 1.8.0_291 on coder4deMacBook-Pro.local with PID 2806 (/Users/coder4/Downloads/homs-demo/build/libs/homs-demo-0.0.1-SNAPSHOT.jar started by coder4 in /Users/coder4/Downloads/homs-demo)\n2021-09-08 12:47:51.909  INFO 2806 --- [           main] com.coder4.homsdemo.HomsDemoApplication  : No active profile set, falling back to default profiles: default\n2021-09-08 12:47:52.960  INFO 2806 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)\n2021-09-08 12:47:52.975  INFO 2806 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]\n2021-09-08 12:47:52.975  INFO 2806 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.52]\n2021-09-08 12:47:53.032  INFO 2806 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext\n2021-09-08 12:47:53.032  INFO 2806 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1067 ms\n2021-09-08 12:47:53.413  INFO 2806 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''\n2021-09-08 12:47:53.424  INFO 2806 --- [           main] com.coder4.homsdemo.HomsDemoApplication  : Started HomsDemoApplication in 1.951 seconds (JVM running for 2.388)\n```\n\n我们在浏览器打开 http:127.0.0.1:8080 已经可以成功打开了！\n\n在微服务架构中，需要新建大量微服务。而Spring社区提供的Starter工具，降低了微服务的初始化门槛。在实际开发中，我们也可以结合实际情况，定制出适合自己团队的脚手架工具。\n\n## 子项目改造\n\n上述脚手架生成的项目，是独立项目模式：一个目录下，只有一个独立项目。\n\n在实际微服务开发中，一个目录下需要多组相互关联的子项目，例如：\n\n- protobuf和桩文件单独拆成子项目\n\n- 常量提取到单独子项目\n\n在本书的实战中，我们的微服务选用的是server / client 双子项目结构\n\n- client：内置protobuf、桩文件，客户端代码、自动配置代码\n\n- server：专注服务端逻辑开发\n\n将Gradle项目拆分为子项目的功能，网上资料不多，自己摸索需要踩很多坑。\n\n本文提供的也只是一种实现方式，你可以在此基础上，进行改造。\n\n先看下整体目录结构：\n\n```shell\n./├── build.gradle\n├── gradle\n│   └── wrapper\n│       ├── gradle-wrapper.jar\n│       └── gradle-wrapper.properties\n├── gradlew\n├── gradlew.bat\n├── homs-demo-client\n│   ├── build.gradle\n│   └── src\n│       └── main\n│           └── java\n│               └── com\n│                   └── coder4\n│                       └── homs\n│                           └── demo\n│                               ├── HomsDemo.proto\n│                               ├── HomsDemoGrpc.java\n│                               ├── HomsDemoProto.java\n│                               └── client\n│                                   └── HomsDemoClient.java\n├── homs-demo-server\n│   ├── build.gradle\n│   └── src\n│       ├── main\n│       │   ├── java\n│       │   │   └── com\n│       │   │       └── coder4\n│       │   │           └── homs\n│       │   │               └── demo\n│       │   │                   └── server\n│       │   │                       ├── Application.java\n│       │   └── resources\n│       │       └── application.yaml\n│       └── test\n│           └── java\n│               └── com\n│                   └── coder4\n│                       └── homs\n│                           └── demo\n│                               └── server\n│                                   └── Test.java\n└── settings.gradle\n```\n\n如上图所述，我们在独立项目的基础上，改造如下：\n\n- 新增homs-demo-client / homs-demo-server 两个子项目\n\n- 子项目内，额外添加了build.gradle文件\n\n下面我们来看下gradle的相关配置\n\n首先是根目录下的\n\nsettings.gradle\n\n```groovy\nrootProject.name = 'homs-demo'\ninclude 'homs-demo-client'\ninclude 'homs-demo-server'\n```\n\n如上所述，定义了项目名为\"homs-demo\"，两个子项目\"homs-demo-client\" 和 \"homs-demo-server\"。\n\n接着看一下根目录下的\n\nbuild.gradle\n\n```groovy\nplugins {\n\n  id 'java'\n  id 'idea'\n  id 'org.springframework.boot' version '2.5.3' apply false\n  id 'io.spring.dependency-management' version '1.0.11.RELEASE' apply false\n\n}\n\nsubprojects {\n\n  group = 'com.coder4'\n  version = '0.0.1-SNAPSHOT'\n  sourceCompatibility = '1.8'\n\n}\n```\n\n这里的plugin部分，定义了4个插件：\n\n- java：java项目必选\n\n- idea (Intellj IDEA)：生成idea需要的文件\n\n- org.springframework.boot：Spring Boot插件，支持构建可执行的server.jar\n\n- io.spring.dependency-management：Spring Boot相关版本的依赖管理\n\nsubprojects部分定义了所以子项目(server / client)的公用参数\n\n- group / version 项目包名和版本\n\n- sourceCompatibility：Java 8的语言版本\n\n我们再来看一下client子项目\n\nhoms-demo-client/build.gradle\n\n```groovy\nplugins {\n    id 'java'\n    id 'io.spring.dependency-management'\n}\n\ndependencies {\n    implementation \"org.slf4j:slf4j-api:1.7.32\"\n}\n```\n\n上述是client子项目的gradle配置，不难发现：\n\n- plugins：java、spring依赖\n\n- dependencies：这里的配置等同于maven的pom.xml中的依赖配置，但gradle以冒号分割的语法更加简洁。这里只配置了一个slf4j。\n\n再看下server子项目\n\n```groovy\nplugins {\n    id 'java'\n    id 'org.springframework.boot'\n    id 'io.spring.dependency-management'\n}\n\ndependencies {\n    implementation project(':homs-demo-client')\n    implementation 'org.slf4j:slf4j-api:1.7.32'\n    implementation 'org.springframework.boot:spring-boot-starter-web'\n}\n```\n\nserver与client有所不同：\n\n- plugins：增加了spring boot插件\n\n- dependencies：首先依赖了客户端子项目，接着依赖Spring Boot的web-starter。\n\n你可能已经注意到了，在server的依赖中，并没有设定spring-boot-starter-web的版本。\n\nSpring相关依赖的版本补全由'dependency-management'插件自动处理。当我们在项目根路径的build.gradle中，声明Spring Boot插件和Dependency Management时，就确定了所有子项目中，Spring依赖的版本。\n\n经过上述改造，我们已经“基本”完成了子项目的改造。\n\n## 实现BOM功能\n\n为什么我们说“基本”完成呢？\n\n因为，子项目改造引入了新的问题：\n\n若在client和server中，各自依赖slf4j但版本不同，会发生什么情况？\n\n没错，这就是经典的“Maven依赖冲突”问题，关于背景和常见解法可以参考[这篇]([Solving Dependency Conflicts in Maven - DZone Java](https://dzone.com/articles/solving-dependency-conflicts-in-maven))文章。\n\n依赖冲突问题的最根本解法是：让大家都依赖于相同的版本。在Maven中可以使用bom清单(bill of material)：将所有公用包的版本都声明在bom文件中，然后其余项目都依赖bom。\n\nGradle并没有直接实现BOM，但在6.0+支持了platform机制。它可以实现与BOM类似的效果。\n\n我们新建一个独立的项目，bom-homs\n\nsettings.gradle \n\n```groovy\nrootProject.name = 'bom-homs'\n```\n\n这里声明了bom的名字\n\nbuild.gradle\n\n```groovy\nplugins {\n    id 'java-platform'\n    id 'maven-publish'\n}\n\ngroup 'com.coder4'\nversion '1.0'\n\ndependencies {\n    constraints {\n        api 'org.slf4j:slf4j-api:1.7.32'\n    }\n}\n\npublishing {\n    publications {\n        myPlatform(MavenPublication) {\n            from components.javaPlatform\n        }\n    }\n}\n```\n\n上述配置的解析如下：\n\n- plugins：platform和maven发布插件\n\n- group、version：maven中同等概念，一会用到\n\n- dependencies：公用包的版本声明，这里只又一个slf4j\n\n- publishing：这里借用了Maven的发布方式\n\n下面我们执行发布(到本地)：\n\n```shell\ngradle publishToMavenLocal\n\nBUILD SUCCESSFUL in 704ms\n3 actionable tasks: 3 executed\n```\n\n（这里我们暂时发布到本地，如何发布到远程、私有仓库，将在后续章节再介绍。）\n\n成功发布后，我们回到homs-demo项目中，将server的子项目改造如下：\n\n```groovy\nplugins {\n    id 'java'\n    id 'org.springframework.boot'\n    id 'io.spring.dependency-management'\n}\n\ndependencies {\n\n    implementation project(':homs-demo-client')\n    implementation platform('com.coder4:bom-homs:1.0')\n\n    implementation 'org.slf4j:slf4j-api'\n    implementation 'org.springframework.boot:spring-boot-starter-web'\n}\n```\n\n通过引入platform，我们就无需在项目中指明slf4j的版本了，从而在源头上解决了版本冲突的问题！\n\n针对client子项目，也是类似的修改，这里不做赘述。\n\n至此，我们完成Gradle与Spring Boot的集成、子项目拆分。\n\n关于“Spring Boot + Gradle子项目”的资料，在网上并不多见，希望你能仔细阅读、反复揣摩、举一反三:-)\n\n本文涉及的项目代码，我整理到了[这里](https://github.com/liheyuan/homs-demo)，供大家参考。\n"
  },
  {
    "path": "src/ch03-ms-dev2/README.md",
    "content": "# 微服务开发中篇：微服务的注册与发现、配置中心、消息队列、稳定性\n\n你可能留意到，在\"微服务上篇\"的讨论中，我们介绍的RPC、数据库等内容，都局限于单机环境，并没有真正涉及“分布式”。\n\n在本章，我们将\"真正的\"进入分布式的微服务实战开发。\n\n在微服务的架构下，经过服务的拆分，会形成复杂的服务调用关系，例如A调用B，B调用C....调用Z。同时，出于性能考虑，每一个服务X可能由若干个实例组成。如此庞大的实例数量，如果依靠手工配置来管理，是一个不可能完成的任务。为此，我们需要引入微服务的注册中心。\n\n我们将基于Nacos来实现服务的注册与发现：Nacos的基本用法、服务端的自动注册，客户端的自动发现、装配。\n\nNacos不仅是服务管理平台，也提供了配置管理的功能，我们将基于此实现微服务的配置中心。\n\n消息队列是应用接耦、流量消峰的利器，我们将介绍Rocket MQ的基础概念，并将其集成进开发框架中。\n\n保证微服务的稳定性有三大法宝：“熔断、限流、降级”。在本章的最后，我们将引入轻量但强大的resilience4j，为微服务保驾护航。\n"
  },
  {
    "path": "src/ch03-ms-dev2/circuit-breaker-and-limiter.md",
    "content": "# Spring Boot集成熔断、限流、降级\n\n在引入resilience4j之前，我们先来讨论下服务稳定性的三大法宝。\n\n- 降级：在有限资源情况下，为了应对超负荷流量，适当放弃一些功能，以保证服务的整体稳定性。例如：双十一大促时，关闭个性化推荐。\n\n- 限流：为了应对突发流量，只允许一部分请求通过，放弃其余请求。例如：当前服务忙，请稍后再试。\n\n- 熔断：这个概念最早源于物理学。\n  \n  - 在电路中，若电流过大，熔断器(保险丝 / 空气开关)会发生熔断，切断线路，以保证用电安全。\n  \n  - 在微服务架构中，若服务调用发生大量错误(超时)，可以直接将微服务降级，以保证服务的整体稳定性。\n\nResillence4j是一款轻量级、易用的\"容错框架\"，提供了保证稳定性所需的几大基础组件：\n\n- Retry：重试\n\n- Circuit Breaker：基于Ring Buffer的熔断器，根据失败率/次数，自动切换熔断器的开关状态。\n\n- Rate Limiter：基于AtomicReference + 状态机 实现的限流器\n\n- Time Limiter：基于限时Future / CompletationStage的时限器\n\n- Bulk Head：基于信号量 / 线程池的壁仓隔离。\n\n- Cache / Fallback：为上述组件提供降级时的包装函数\n\nResillence4j支持Java、注解等多种使用方法，我们这里选用最方便的Spring Boot注解方法。\n\n## Circuit Breaker\n\n首先来看一下熔断器，它内置了如下三种状态：\n\n- CLOSE：初始状态，熔断器关闭，服务正常运行。\n\n- OPEN：发生大量错误后，熔断器打开，直接返回降级结果，不再调用真实服务逻辑。\n\n- HALF OPEN：OPEN一段时间后，小流量放开访问，看真实逻辑部分是否恢复正常。如果恢复，会切换到CLOSE状态。\n\n老规矩，先添加依赖：\n\n```groovy\nimplementation 'io.github.resilience4j:resilience4j-all:1.7.1'\nimplementation 'io.github.resilience4j:resilience4j-spring-boot2:1.7.1'\n```\n\n说明如下：\n\n- 由于后续几个组件都会使用，我们这里直接使用了all，你可以根据实际情况，裁剪需要的组件。\n\n- spring-boot：添加了对应的注解和自动配置。\n\n熔断器的配置如下：\n\n```yaml\nresilience4j:\n  circuitbreaker:\n    instances:\n      getUserById:\n        registerHealthIndicator: true\n        slidingWindowSize: 100\n        failureRateThreshold: 50\n```\n\n说明如下：\n\n- 熔断器名称是getUserById\n\n- 滑动窗口大小100\n\n- 失败(熔断)阀值是50%\n\n代码用法如下：\n\n```java\n  @Override\n    @CircuitBreaker(name = \"getUser\", fallbackMethod = \"getUserByIdFallback\")\n    public Optional<User> getUserById(long id) {\n        // Mock a failure\n        if (ThreadLocalRandom.current().nextInt(100) < 90) {\n            throw new RuntimeException(\"mock failure\");\n        }\n        return userRepository.getUser(id);\n    }\n\n    public Optional<User> getUserByIdFallback(long id, Throwable e) {\n        LOG.error(\"enter fallback for getUserById\", e);\n        return Optional.empty();\n    }\n```\n\n在上述代码中，我们以90%的概率模拟了随机异常。\n\n当熔断发生时，会使用getUserByIdFallback中的降级结果。\n\n执行几次后，会出现类似如下的错误日志，熔断器已成功开启。\n\n```shell\n2021-10-09 01:34:32.156 ERROR 2214 --- [o-8080-exec-144] c.c.h.d.s.service.impl.UserServiceImpl   : enter fallback for getUserById\n\nio.github.resilience4j.circuitbreaker.CallNotPermittedException: CircuitBreaker 'getUser' is OPEN and does not permit further calls\n    at io.github.resilience4j.circuitbreaker.CallNotPermittedException.createCallNotPermittedException(CallNotPermittedException.java:48) ~[resilience4j-circuitbreaker-1.7.1.jar:1.7.1]\n    at io.github.resilience4j.circuitbreaker.internal.CircuitBreakerStateMachine$OpenState.acquirePermission(CircuitBreakerStateMachine.java:696) ~[resilience4j-circuitbreaker-1.7.1.jar:1.7.1]\n    at io.github.resilience4j.circuitbreaker.internal.CircuitBreakerStateMachine.acquirePermission(CircuitBreakerStateMachine.java:206) ~[resilience4j-circuitbreaker-1.7.1.jar:1.7.1]\n    at io.github.resilience4j.circuitbreaker.CircuitBreaker.lambda$decorateCheckedSupplier$82a9021a$1(CircuitBreaker.java:70) ~[resilience4j-circuitbreaker-1.7.1.jar:1.7.1]\n    at io.github.resilience4j.circuitbreaker.CircuitBreaker.executeCheckedSupplier(CircuitBreaker.java:834) ~[resilience4j-circuitbreaker-1.7.1.jar:1.7.1]\n    at io.github.resilience4j.circuitbreaker.configure.CircuitBreakerAspect.defaultHandling(CircuitBreakerAspect.java:188) ~[resilience4j-spring-1.7.1.jar:1.7.1]\n    at io.github.resilience4j.circuitbreaker.configure.CircuitBreakerAspect.proceed(CircuitBreakerAspect.java:135) ~[resilience4j-spring-1.7.1.jar:1.7.1]\n    at io.github.resilience4j.circuitbreaker.configure.CircuitBreakerAspect.lambda$circuitBreakerAroundAdvice$6edadc33$1(CircuitBreakerAspect.java:118) ~[resilience4j-spring-1.7.1.jar:1.7.1]\n    at io.github.resilience4j.fallback.DefaultFallbackDecorator.lambda$decorate$52452fd9$1(DefaultFallbackDecorator.java:36) ~[resilience4j-spring-1.7.1.jar:1.7.1]\n    at io.github.resilience4j.circuitbreaker.configure.CircuitBreakerAspect.circuitBreakerAroundAdvice(CircuitBreakerAspect.java:118) ~[resilience4j-spring-1.7.1.jar:1.7.1]\n    at sun.reflect.GeneratedMethodAccessor127.invoke(Unknown Source) ~[na:na]\n    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_291]\n    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_291]\n    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:634) [spring-aop-5.3.9.jar:5.3.9]\n    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:624) [spring-aop-5.3.9.jar:5.3.9]\n    at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:72) [spring-aop-5.3.9.jar:5.3.9]\n    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175) [spring-aop-5.3.9.jar:5.3.9]\n    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) [spring-aop-5.3.9.jar:5.3.9]\n    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) [spring-aop-5.3.9.jar:5.3.9]\n    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) [spring-aop-5.3.9.jar:5.3.9]\n    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) [spring-aop-5.3.9.jar:5.3.9]\n    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692) [spring-aop-5.3.9.jar:5.3.9]\n    at com.coder4.homs.demo.server.service.impl.UserServiceImpl$$EnhancerBySpringCGLIB$$19b58f1b.getUserById(<generated>) [main/:na]\n    at com.coder4.homs.demo.server.web.logic.impl.UserLogicImpl.getUserById(UserLogicImpl.java:51) [main/:na]\n    at com.coder4.homs.demo.server.web.ctrl.UserController.getById(UserController.java:36) [main/:na]\n    at sun.reflect.GeneratedMethodAccessor113.invoke(Unknown Source) ~[na:na]\n    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_291]\n    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_291]\n    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197) [spring-web-5.3.9.jar:5.3.9]\n    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141) [spring-web-5.3.9.jar:5.3.9]\n    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106) [spring-webmvc-5.3.9.jar:5.3.9]\n    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) [spring-webmvc-5.3.9.jar:5.3.9]\n    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) [spring-webmvc-5.3.9.jar:5.3.9]\n    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) [spring-webmvc-5.3.9.jar:5.3.9]\n    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1064) [spring-webmvc-5.3.9.jar:5.3.9]\n    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) [spring-webmvc-5.3.9.jar:5.3.9]\n    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) [spring-webmvc-5.3.9.jar:5.3.9]\n    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) [spring-webmvc-5.3.9.jar:5.3.9]\n    at javax.servlet.http.HttpServlet.service(HttpServlet.java:655) [tomcat-embed-core-9.0.50.jar:4.0.FR]\n    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) [spring-webmvc-5.3.9.jar:5.3.9]\n    at javax.servlet.http.HttpServlet.service(HttpServlet.java:764) [tomcat-embed-core-9.0.50.jar:4.0.FR]\n    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:228) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) [tomcat-embed-websocket-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) [spring-web-5.3.9.jar:5.3.9]\n    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.3.9.jar:5.3.9]\n    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) [spring-web-5.3.9.jar:5.3.9]\n    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.3.9.jar:5.3.9]\n    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:96) [spring-boot-actuator-2.5.3.jar:2.5.3]\n    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.3.9.jar:5.3.9]\n    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) [spring-web-5.3.9.jar:5.3.9]\n    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.3.9.jar:5.3.9]\n    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1723) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_291]\n    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_291]\n    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at java.lang.Thread.run(Thread.java:748) [na:1.8.0_291]\n```\n\n## Bulkhead & TimeLimiter\n\n下面我们来看一下实线器，即限定必须在X时间内执行完毕，否则抛出异常。\n\nResillence4j的TimeLimiter设计中，并没有内置线程池，而是要业务代码自行处理。我们可以结合Bulkhead的线程池模式一同使用，首先配置如下：\n\n```yaml\nresilience4j:\n  thread-pool-bulkhead:\n    instances:\n      getUserByName:\n        maxThreadPoolSize: 100\n        coreThreadPoolSize: 50\n        queueCapacity: 200\n  timelimiter:\n    instances:\n      getUserByName:\n        timeoutDuration: 1s\n        cancelRunningFuture: true\n```\n\n如上所述，我们配置了线程池，并设置时限为1秒。\n\n接着看一下用法：\n\n```java\n@Override\n@Bulkhead(name = \"getUserByName\", type = Type.THREADPOOL)\n@TimeLimiter(name = \"getUserByName\", fallbackMethod = \"getUserByNameWithCompletableFutureFallback\")\npublic CompletableFuture<Optional<User>> getUserByNameWithCompletableFuture(String name) {\n    // Mock timeout\n    Try.run(() -> Thread.sleep(ThreadLocalRandom.current().nextInt(2000)));\n\n    return CompletableFuture.completedFuture(userRepository.getUserByName(name));\n}\n\npublic CompletableFuture<Optional<User>> getUserByNameWithCompletableFutureFallback(String name, Throwable e) {\n    LOG.error(\"enter fallback for getUserByNameFallback\", e);\n    return CompletableFuture.completedFuture(Optional.empty());\n}\n```\n\n我们模拟了随机超时时间，当超过1秒时，会自动抛出如下的降级异常，并走降级逻辑。\n\n```shell\n2021-10-09 01:53:32.637 ERROR 4890 --- [pool-7-thread-1] c.c.h.d.s.service.impl.UserServiceImpl   : enter fallback for getUserByNameFallback\n\njava.util.concurrent.TimeoutException: TimeLimiter 'getUserByName' recorded a timeout exception.\n    at io.github.resilience4j.timelimiter.TimeLimiter.createdTimeoutExceptionWithName(TimeLimiter.java:221) ~[resilience4j-timelimiter-1.7.1.jar:1.7.1]\n    at io.github.resilience4j.timelimiter.internal.TimeLimiterImpl$Timeout.lambda$of$0(TimeLimiterImpl.java:185) ~[resilience4j-timelimiter-1.7.1.jar:1.7.1]\n    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) ~[na:1.8.0_291]\n    at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_291]\n    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_291]\n    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [na:1.8.0_291]\n    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_291]\n    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_291]\n    at java.lang.Thread.run(Thread.java:748) [na:1.8.0_291]\n```\n\n## RateLimiter\n\n最后我们来看一下限流器，配置如下：\n\n```yaml\nresilience4j:\n  rateLimiter:\n    instances:\n      getUserByIdV2:\n        limitForPeriod: 1\n        limitRefreshPeriod: 500ms\n        timeoutDuration: 0\n```\n\n设置了每0.5秒限1个请求，用法如下：\n\n```java\n@Override\n@RateLimiter(name = \"getUserByIdV2\", fallbackMethod = \"getUserByIdV2Fallback\")\npublic Optional<User> getUserByIdV2(long id) {\n    return Optional.ofNullable(userMapper.getUser(id)).map(UserDO::toUser);\n}\n\npublic Optional<User> getUserByIdV2Fallback(long id, Throwable e) {\n    LOG.error(\"getUserByIdV2 fallback exception\", e);\n    return Optional.empty();\n}\n```\n\n当快速访问两次接口后，会抛出如下的异常，并返回降级结果。\n\n```shell\n2021-10-09 14:00:13.564 ERROR 5598 --- [nio-8080-exec-8] c.c.h.d.s.service.impl.UserServiceImpl   : getUserByIdV2 fallback exception\n\nio.github.resilience4j.ratelimiter.RequestNotPermitted: RateLimiter 'getUserByIdV2' does not permit further calls\n    at io.github.resilience4j.ratelimiter.RequestNotPermitted.createRequestNotPermitted(RequestNotPermitted.java:43) ~[resilience4j-ratelimiter-1.7.1.jar:1.7.1]\n    at io.github.resilience4j.ratelimiter.RateLimiter.waitForPermission(RateLimiter.java:591) ~[resilience4j-ratelimiter-1.7.1.jar:1.7.1]\n    at io.github.resilience4j.ratelimiter.RateLimiter.lambda$decorateCheckedSupplier$9076412b$1(RateLimiter.java:213) ~[resilience4j-ratelimiter-1.7.1.jar:1.7.1]\n    at io.github.resilience4j.ratelimiter.RateLimiter.executeCheckedSupplier(RateLimiter.java:898) ~[resilience4j-ratelimiter-1.7.1.jar:1.7.1]\n    at io.github.resilience4j.ratelimiter.RateLimiter.executeCheckedSupplier(RateLimiter.java:884) ~[resilience4j-ratelimiter-1.7.1.jar:1.7.1]\n    at io.github.resilience4j.ratelimiter.configure.RateLimiterAspect.handleJoinPoint(RateLimiterAspect.java:179) ~[resilience4j-spring-1.7.1.jar:1.7.1]\n    at io.github.resilience4j.ratelimiter.configure.RateLimiterAspect.proceed(RateLimiterAspect.java:142) ~[resilience4j-spring-1.7.1.jar:1.7.1]\n    at io.github.resilience4j.ratelimiter.configure.RateLimiterAspect.lambda$rateLimiterAroundAdvice$749d37c4$1(RateLimiterAspect.java:125) ~[resilience4j-spring-1.7.1.jar:1.7.1]\n    at io.github.resilience4j.fallback.DefaultFallbackDecorator.lambda$decorate$52452fd9$1(DefaultFallbackDecorator.java:36) ~[resilience4j-spring-1.7.1.jar:1.7.1]\n    at io.github.resilience4j.ratelimiter.configure.RateLimiterAspect.rateLimiterAroundAdvice(RateLimiterAspect.java:125) ~[resilience4j-spring-1.7.1.jar:1.7.1]\n    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_291]\n    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_291]\n    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_291]\n    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_291]\n    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:634) ~[spring-aop-5.3.9.jar:5.3.9]\n    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:624) ~[spring-aop-5.3.9.jar:5.3.9]\n    at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:72) [spring-aop-5.3.9.jar:5.3.9]\n    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175) [spring-aop-5.3.9.jar:5.3.9]\n    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) [spring-aop-5.3.9.jar:5.3.9]\n    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) [spring-aop-5.3.9.jar:5.3.9]\n    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) [spring-aop-5.3.9.jar:5.3.9]\n    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) [spring-aop-5.3.9.jar:5.3.9]\n    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692) [spring-aop-5.3.9.jar:5.3.9]\n    at com.coder4.homs.demo.server.service.impl.UserServiceImpl$$EnhancerBySpringCGLIB$$cba2db53.getUserByIdV2(<generated>) [main/:na]\n    at com.coder4.homs.demo.server.web.logic.impl.UserLogicImpl.getUserByIdV2(UserLogicImpl.java:80) [main/:na]\n    at com.coder4.homs.demo.server.web.ctrl.UserController.getByIdV2(UserController.java:51) [main/:na]\n    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_291]\n    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_291]\n    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_291]\n    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_291]\n    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197) [spring-web-5.3.9.jar:5.3.9]\n    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141) [spring-web-5.3.9.jar:5.3.9]\n    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106) [spring-webmvc-5.3.9.jar:5.3.9]\n    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) [spring-webmvc-5.3.9.jar:5.3.9]\n    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) [spring-webmvc-5.3.9.jar:5.3.9]\n    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) [spring-webmvc-5.3.9.jar:5.3.9]\n    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1064) [spring-webmvc-5.3.9.jar:5.3.9]\n    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) [spring-webmvc-5.3.9.jar:5.3.9]\n    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) [spring-webmvc-5.3.9.jar:5.3.9]\n    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) [spring-webmvc-5.3.9.jar:5.3.9]\n    at javax.servlet.http.HttpServlet.service(HttpServlet.java:655) [tomcat-embed-core-9.0.50.jar:4.0.FR]\n    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) [spring-webmvc-5.3.9.jar:5.3.9]\n    at javax.servlet.http.HttpServlet.service(HttpServlet.java:764) [tomcat-embed-core-9.0.50.jar:4.0.FR]\n    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:228) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) [tomcat-embed-websocket-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) [spring-web-5.3.9.jar:5.3.9]\n    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.3.9.jar:5.3.9]\n    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) [spring-web-5.3.9.jar:5.3.9]\n    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.3.9.jar:5.3.9]\n    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:96) [spring-boot-actuator-2.5.3.jar:2.5.3]\n    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.3.9.jar:5.3.9]\n    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) [spring-web-5.3.9.jar:5.3.9]\n    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.3.9.jar:5.3.9]\n    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:190) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:163) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1723) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_291]\n    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_291]\n    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.50.jar:9.0.50]\n    at java.lang.Thread.run(Thread.java:748) [na:1.8.0_291]\n```\n\n至此，我们已经熟悉了Resillence4j中的主要组件，并覆盖了yaml中的常见的配置。\n\n更多配置选项，可以参考[这篇文档](https://resilience4j.readme.io/docs/getting-started-3)。\n\n由于篇幅限制，本文并未涉及Retry、Cache两大组件，推荐你阅读[官方文档](https://resilience4j.readme.io/docs/retry)自行探索。\n"
  },
  {
    "path": "src/ch03-ms-dev2/config.md",
    "content": "# Spring Boot集成配置中心\n\nNacos不仅提供了服务的注册与发现，也提供了配置管理的功能。\n\n本节，我们继续使用Nacos，基于其配置管理的功能，实现微服务的配置中心。\n\n首先，我们在Nacos上，新建两个配置：\n\n![f](./nacos-config.png)\n\n如上图所示：\n\n- Nacos提供了dataId、group两个字段，用于区分不同的配置\n\n- 我们在group字段填充微服务的名称，例如homs-demo\n\n- 我们在dataId字段填写配置的key\n\n- Nacos的支持简单的类型检验，例如json、数值、字符串等，但只限于前端校验，存储后多统一为字符串类型\n\n有了配置后，我们来实现Nacos配置管理的驱动部分：\n\n```java\npublic interface NacosConfigService {\n\n    Optional<String> getConfig(String serviceName, String key);\n\n    void onChange(String serviceName, String key, Consumer<Optional<String>> consumer);\n\n}\n```\n\n```java\npackage com.coder4.homs.demo.server.service.impl;\n\nimport com.alibaba.nacos.api.NacosFactory;\nimport com.alibaba.nacos.api.config.ConfigService;\nimport com.alibaba.nacos.api.config.listener.Listener;\nimport com.alibaba.nacos.api.exception.NacosException;\nimport com.coder4.homs.demo.server.service.spi.NacosConfigService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\nimport javax.annotation.PostConstruct;\nimport java.util.Optional;\nimport java.util.concurrent.Executor;\nimport java.util.function.Consumer;\n\n/**\n * @author coder4\n */\n@Service\npublic class NacosConfigServiceImpl implements NacosConfigService{\n\n    private static final Logger LOG = LoggerFactory.getLogger(NacosConfigServiceImpl.class);\n\n    @Value(\"${nacos.server}\")\n    private String nacosServer;\n\n    private ConfigService configService;\n\n    @PostConstruct\n    public void postConstruct() throws NacosException {\n        configService = NacosFactory\n                .createConfigService(nacosServer);\n    }\n\n    @Override\n    public Optional<String> getConfig(String serviceName, String key) {\n        try {\n            return Optional.ofNullable(configService.getConfig(key, serviceName, 5000));\n        } catch (NacosException e) {\n            LOG.error(\"nacos get config exception for \" + serviceName + \" \" + key, e);\n            return Optional.empty();\n        }\n    }\n\n    @Override\n    public void onChange(String serviceName, String key, Consumer<Optional<String>> consumer) {\n        try {\n            configService.addListener(key, serviceName, new Listener() {\n                @Override\n                public Executor getExecutor() {\n                    return null;\n                }\n\n                @Override\n                public void receiveConfigInfo(String configInfo) {\n                    consumer.accept(Optional.ofNullable(configInfo));\n                }\n            });\n        } catch (NacosException e) {\n            LOG.error(\"nacos add listener exception for \" + serviceName + \" \" + key, e);\n            throw new RuntimeException(e);\n        }\n    }\n}\n```\n\n上述驱动部分，主要实现了两个功能：\n\n- 通过getConfig方法，同步拉取配置\n\n- 通过onChange方法，添加异步监听器，当配置发生改变时，会执行回调\n\n## 配置的自动注解与更新\n\n我们希望实现一个更加“易用”的配置中心，期望具有如下特性：\n\n- 通过注解的方式，自动将类中的字段\"绑定\"到远程Nacos配置中心对应字段上，并自动初始化。\n\n- 当Nacos配置更新后，本地同步进行修改。\n\n- 支持类型的自动转换\n\n第一步，我们声明注解：\n\n```java\npackage com.coder4.homs.demo.server.annotation;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Target({ElementType.FIELD, ElementType.PARAMETER})\n@Retention(RetentionPolicy.RUNTIME)\n@Documented\npublic @interface HSConfig {\n\n    String name() default \"\";\n\n    String serviceName() default \"\";\n\n}\n```\n\n上述关键字段的用途是：\n\n- name，远程fdc指定的配置名称，可选，若未填写则使用注解应用的原始字段名。\n\n- serviceName，远程fdc指定的服务名称，可选，若未填写则使用当前本地服务名。\n\n接着，我们借助BeanPostProcessor，来对打了HSConfig注解的字段，进行值注入。\n\n```java\npackage com.coder4.homs.demo.server.processor;\n\nimport com.alibaba.nacos.common.utils.StringUtils;\nimport com.coder4.homs.demo.server.HsReflectionUtils;\nimport com.coder4.homs.demo.server.annotation.HSConfig;\nimport com.coder4.homs.demo.server.service.spi.NacosConfigService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.aop.support.AopUtils;\nimport org.springframework.beans.BeansException;\nimport org.springframework.beans.factory.config.BeanPostProcessor;\nimport org.springframework.core.Ordered;\nimport org.springframework.data.util.ReflectionUtils.AnnotationFieldFilter;\nimport org.springframework.util.ReflectionUtils;\nimport org.springframework.util.ReflectionUtils.FieldFilter;\n\nimport java.lang.reflect.Field;\nimport java.util.Optional;\n\n/**\n * @author coder4\n */\npublic class HsConfigFieldProcessor implements BeanPostProcessor, Ordered {\n\n    private static final Logger LOG = LoggerFactory.getLogger(HsConfigFieldProcessor.class);\n\n    private static final FieldFilter HS_CONFIG_FIELD_FILTER = new AnnotationFieldFilter(HSConfig.class);\n\n    private NacosConfigService nacosConfigService;\n\n    private String serviceName;\n\n    public HsConfigFieldProcessor(NacosConfigService service, String serviceName) {\n        this.nacosConfigService = service;\n        this.serviceName = serviceName;\n    }\n\n    @Override\n    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {\n        Class targetClass = AopUtils.getTargetClass(bean);\n        ReflectionUtils.doWithFields(\n                targetClass, field -> processField(bean, field), HS_CONFIG_FIELD_FILTER);\n        return bean;\n    }\n\n    private void processField(Object bean, Field field) {\n        HSConfig valueAnnotation = field.getDeclaredAnnotation(HSConfig.class);\n        // 优先注解，其次本地代码\n        String key = StringUtils.defaultIfEmpty(valueAnnotation.name(), field.getName());\n        String serviceName = StringUtils.defaultIfEmpty(valueAnnotation.serviceName(), this.serviceName);\n        Optional<String> valueOp = nacosConfigService.getConfig(serviceName, key);\n        try {\n            if (!valueOp.isPresent()) {\n                LOG.error(\"nacos config for serviceName = {} key = {} is empty\", serviceName, key);\n            }\n            HsReflectionUtils.setField(bean, field, valueOp.get());\n\n            // Future Change\n            nacosConfigService.onChange(serviceName, key, valueOp2 -> {\n                try {\n                    HsReflectionUtils.setField(bean, field, valueOp2.get());\n                } catch (IllegalAccessException e) {\n                    LOG.error(\"nacos config for serviceName = {} key = {} exception\", e);\n                }\n            });\n        } catch (IllegalAccessException e) {\n            LOG.error(\"setField for \" + field.getName() + \" exception\", e);\n            throw new RuntimeException(e.getMessage());\n        }\n    }\n\n    @Override\n    public int getOrder() {\n        return LOWEST_PRECEDENCE;\n    }\n}\n```\n\n上述代码比较复杂，我们逐步讲解：\n\n- 构造函数传入nacosConfigService用于操作nacos配置管理接口\n\n- 构造函数传入的serviceName做为默认的服务名\n\n- postProcessBeforeInitialization方法，会在Bean构造前执行，通过ReflectionUtils来过滤所有打了@HsConfig注解的字段，逐一处理，流程如下：\n  \n  - 首先获取要绑定的服务名、字段名，遵循注解优于本地的顺序\n  \n  - 调用nacosServer拉取当前配置，并通过HsReflectionUtils工具的反射的注入到字段中。\n  \n  - 添加回调，以便未来更新时，及时修改本地变量。\n\nHsReflectionUtils中涉及类型的自动转换，代码如下：\n\n```java\npackage com.coder4.homs.demo.server.utils;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\n\nimport java.lang.reflect.Field;\n\n/**\n * @author coder4\n */\npublic class HsReflectionUtils {\n\n    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();\n\n    public static void setField(Object bean, Field field, String valueStr) throws IllegalAccessException {\n        field.setAccessible(true);\n        Class fieldType = field.getType();\n        if (fieldType == Integer.TYPE || fieldType == Integer.class) {\n            field.set(bean, Integer.parseInt(valueStr));\n        } else if (fieldType == Long.TYPE || fieldType == Long.class) {\n            field.set(bean, Long.parseLong(valueStr));\n        } else if (fieldType == Short.TYPE || fieldType == Short.class) {\n            field.set(bean, Short.parseShort(valueStr));\n        } else if (fieldType == Double.TYPE || fieldType == Double.class) {\n            field.set(bean, Double.parseDouble(valueStr));\n        } else if (fieldType == Float.TYPE || fieldType == Float.class) {\n            field.set(bean, Float.parseFloat(valueStr));\n        } else if (fieldType == Byte.TYPE || fieldType == Byte.class) {\n            field.set(bean, Byte.parseByte(valueStr));\n        } else if (fieldType == Boolean.TYPE || fieldType == Boolean.class) {\n            field.set(bean, Boolean.parseBoolean(valueStr));\n        } else if (fieldType == Character.TYPE || fieldType == Character.class) {\n            if (valueStr == null || valueStr.isEmpty()) {\n                throw new IllegalArgumentException(\"can't parse char because value string is empty\");\n            }\n            field.set(bean, valueStr.charAt(0));\n        } else if (fieldType.isEnum()) {\n            field.set(bean, Enum.valueOf(fieldType, valueStr));\n        } else {\n            try {\n                field.set(bean, OBJECT_MAPPER.readValue(valueStr, fieldType));\n            } catch (JsonProcessingException e) {\n                throw new IllegalArgumentException(\"can't parse json because exception\");\n            }\n        }\n    }\n\n}\n```\n\n上述代码中，针对field的类型逐一判断，针对八大基本类型，直接parse，针对复杂类型，使用json反序列化的方式注入。\n\n## 自动配置的使用\n\n有了上述的基础后，我们还需要添加自动配置类，让其生效：\n\n```java\npackage com.coder4.homs.demo.server.configuration;\n\nimport com.coder4.homs.demo.constant.HomsDemoConstant;\nimport com.coder4.homs.demo.server.processor.HsConfigFieldProcessor;\nimport com.coder4.homs.demo.server.service.spi.NacosConfigService;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n/**\n * @author coder4\n */\n@Configuration\npublic class HsConfigProcessorConfiguration {\n\n    @Bean\n    @ConditionalOnMissingBean(HsConfigFieldProcessor.class)\n    public HsConfigFieldProcessor fieldProcessor(@Autowired NacosConfigService configService) {\n        return new HsConfigFieldProcessor(configService, HomsDemoConstant.SERVICE_NAME);\n    }\n\n}\n```\n\n使用时非常简单：\n\n```java\n@Service\npublic class HomsDemoConfig {\n\n    @HSConfig\n    private int num;\n\n    @HSConfig(name = \"mapConfig\")\n    private Map<String, String> map;\n\n    @PostConstruct\n    public void postConstruct() {\n        System.out.println(num);\n        System.out.println(map);\n    }\n\n}\n```\n\n只需要添加HSConfig注解，即可完成远程配置的自动注入、绑定、更新。\n"
  },
  {
    "path": "src/ch03-ms-dev2/mq.md",
    "content": "## Spring Boot集成消息队列\n\n[Apache RocketMQ](https://rocketmq.apache.org/)是由开源的轻量级消息队列，于2017年正式成为Apache顶级项目。\n\n在分布式消息队列中间件领域，最热门的项目是Kafka和RocketMQ：\n\n- Kafka是较早开源的\"消息处理平台\"，在写吞吐量上，有明显优势，更适合处理日志类消息。\n\n- RocketMQ借鉴了部分Kafka的设计思路，并对实时性、大分区数等方面进行了优化，较适合做为业务类的消息。\n\n因此，本书选用RocketMQ做为业务类的消息队列。\n\n### 安装并运行RocketMQ\n\nRocketMQ的容器化比较落后，基本没有可用的镜像版本，我们采用手工单机部署的方式。\n\n首先，下载最新版二进制文件，当前是4.9.1：\n\n```shell\nwget https://dlcdn.apache.org/rocketmq/4.9.1/rocketmq-all-4.9.1-bin-release.zip\n```\n\n完成后，解压缩：\n\n```bash\nunizp rocketmq-all-4.9.1-bin-release.zip\n```\n\n启动Name Server：\n\n```bash\nnohup sh bin/mqnamesrv &\ntail -f ~/logs/rocketmqlogs/namesrv.log\n```\n\n最后启动Broker：\n\n```bash\nnohup sh bin/mqbroker -n 127.0.0.1:9876 &\ntail -f ~/logs/rocketmqlogs/broker.log\n```\n\n如果启动成功，在上述两个日志中，会有如下的日志：\n\n```shell\n2021-10-12 4:30:02 INFO main - tls.client.keyPassword = null\n2021-10-12 4:30:02 INFO main - tls.client.certPath = null\n2021-10-12 4:30:02 INFO main - tls.client.authServer = false\n2021-10-12 4:30:02 INFO main - tls.client.trustCertPath = null\n2021-10-12 4:30:02 INFO main - Using JDK SSL provider\n2021-10-12 4:30:03 INFO main - SSLContext created for server\n2021-10-12 4:30:03 INFO main - Try to start service thread:FileWatchService started:false lastThread:null\n2021-10-12 4:30:03 INFO NettyEventExecutor - NettyEventExecutor service started\n2021-10-12 4:30:03 INFO FileWatchService - FileWatchService service started\n2021-10-12 4:30:03 INFO main - The Name Server boot success. serializeType=JSON\n\n2021-10-12 14:36:09 INFO brokerOutApi_thread_3 - register broker[0]to name server 127.0.0.1:9876 OK\n2021-10-12 14:36:09 ERROR DiskCheckScheduledThread1 - Error when measuring disk space usage, file doesn't exist on this path: /Users/coder4/store/commitlog\n2021-10-12 14:36:18 ERROR StoreScheduledThread1 - Error when measuring disk space usage, file doesn't exist on this path: /Users/coder4/store/commitlog\n2021-10-12 14:36:19 ERROR DiskCheckScheduledThread1 - Error when measuring disk space usage, file doesn't exist on this path: /Users/coder4/store/commitlog\n```\n\n可以发现，NameServer是没有问题的，Broker报了一个\"Error when measuring disk space usage\"的错，这个是当前版本的Bug，不影响使用。\n\n如果想退出服务，可以直接kill，或者执行：\n\n```shell\nsh bin/mqshutdown broker\n\nsh bin/mqshutdown namesrv\n```\n\n## RocketMQ架构简介\n\n在集成RocketMQ之前，先介绍一下RocketMQ的基本架构：\n\n- NameServer：轻量级元信息服务，管理路由信息并提供对应的读写服务\n\n- Broker：支撑TOPIC和QUEUE的存储，支持Push和Pull两种协议，有容错、副本、故障恢复机制。\n\n- Producer：发布端服务，支持分布式部署，并向Broker集群发送\n\n- Consumer：消费端服务，同时支持Push和Pull协议。支持消费、广播、顺序消息等特性。\n\n- Topic：队列，用于区分不同消息。\n\n- Tag：同一个Topic下，可以设定不同Tag(例如前缀)，通过Tag来过滤消息，只保留自己感兴趣的。\n\n在使用Producer和Consumer时，需要指定消费组(Consumer Group)，这是从Kafka中借鉴过来的机制。相同Consumer Group下的实例会共享同一个GroupId，会被认为是对等的、可负载均衡的。事件会随机分发给相同GroupId下的多个实例中。\n\n## 在Spring Boot中集成RocketMQ\n\n首先引入依赖：\n\n```groovy\nimplementation 'org.apache.rocketmq:rocketmq-client:4.9.1'\n```\n\n接着，我们创建生产者的抽象基类：\n\n```java\npackage com.coder4.homs.demo.server.mq;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.apache.rocketmq.client.exception.MQClientException;\nimport org.apache.rocketmq.client.producer.DefaultMQProducer;\nimport org.apache.rocketmq.client.producer.SendCallback;\nimport org.apache.rocketmq.client.producer.SendResult;\nimport org.apache.rocketmq.common.message.Message;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.DisposableBean;\n\nimport javax.annotation.PostConstruct;\nimport java.nio.charset.StandardCharsets;\n\n/**\n * @author coder4\n */\npublic abstract class BaseProducer<T> implements DisposableBean {\n\n    private final Logger LOG = LoggerFactory.getLogger(getClass());\n\n    abstract String getNamesrvAddr();\n\n    abstract String getProducerGroup();\n\n    abstract String getTopic();\n\n    abstract String getTag();\n\n    protected DefaultMQProducer producer;\n\n    private ObjectMapper objectMapper = new ObjectMapper();\n\n    public BaseProducer() {\n        producer = new\n                DefaultMQProducer(getProducerGroup());\n    }\n\n    @PostConstruct\n    public void postConstruct() {\n        producer.setNamesrvAddr(getNamesrvAddr());\n        try {\n            producer.start();\n        } catch (MQClientException e) {\n            LOG.error(\"producer start exception\", e);\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public void destroy() throws Exception {\n        producer.shutdown();\n    }\n\n    protected Message buildMessage(String payload) {\n        return new Message(getTopic(),\n                getTag(),\n                payload.getBytes(StandardCharsets.UTF_8)\n        );\n    }\n\n    public void publish(T payload) {\n        try {\n            String val = objectMapper.writeValueAsString(payload);\n            producer.send(buildMessage(val));\n            LOG.info(\"publish success, topic = {}, tag = {}, msg = {}\", getTopic(), getTag(), val);\n        } catch (Exception e) {\n            LOG.error(\"publish exception\", e);\n        }\n    }\n\n    public void publishAsync(T payload) {\n        try {\n            String val = objectMapper.writeValueAsString(payload);\n            producer.send(buildMessage(val), new SendCallback() {\n                @Override\n                public void onSuccess(SendResult sendResult) {\n                    LOG.info(\"publishAsync success, topic = {}, tag = {}, msg = {}\", getTopic(), getTag(), val);\n                }\n\n                @Override\n                public void onException(Throwable e) {\n                    LOG.error(\"publish async exception\", e);\n                }\n            });\n        } catch (Exception e) {\n            LOG.error(\"publishAsync exception\", e);\n        }\n    }\n\n}\n```\n\n如上所示：\n\n- nameServr、topic、tag由子类组成\n\n- 我们在构造函数中，创建了Producer对象\n\n- postConstruct中：设定了NameServer地址，并启动producer\n\n- publish / publishAsync：发送消息，先根据topic和tag构造消息，然后调用同步 / 异步的接口发送。\n\n- destroy时，停止producer\n\n接下来我们看下Consumer的基类：\n\n```java\n/**\n * @(#)BaseConsumer.java, 10月 12, 2021.\n * <p>\n * Copyright 2021 coder4.com. All rights reserved.\n * CODER4.COM PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.\n */\npackage com.coder4.homs.demo.server.mq;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;\nimport org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;\nimport org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;\nimport org.apache.rocketmq.client.exception.MQClientException;\nimport org.apache.rocketmq.common.message.MessageExt;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.DisposableBean;\nimport org.springframework.util.CollectionUtils;\n\nimport javax.annotation.PostConstruct;\n\n/**\n * @author coder4\n */\npublic abstract class BaseConsumer<T> implements DisposableBean {\n\n    protected final Logger LOG = LoggerFactory.getLogger(getClass());\n\n    private static final int DEFAULT_BATCH_SIZE = 1;\n\n    private static final int MAX_RETRY = 1024;\n\n    abstract String getNamesrvAddr();\n\n    abstract String getConsumerGroup();\n\n    abstract String getTopic();\n\n    abstract String getTag();\n\n    abstract Class<T> getClassT();\n\n    abstract boolean process(T msg);\n\n    private ObjectMapper objectMapper = new ObjectMapper();\n\n    protected DefaultMQPushConsumer consumer;\n\n    public BaseConsumer() {\n        consumer = new\n                DefaultMQPushConsumer(getConsumerGroup());\n    }\n\n    @PostConstruct\n    public void postConstruct() {\n        consumer.setNamesrvAddr(getNamesrvAddr());\n        try {\n            consumer.subscribe(getTopic(), getTag());\n        } catch (MQClientException e) {\n            LOG.error(\"consumer subscribe exception\", e);\n            throw new RuntimeException(e);\n        }\n        consumer.setConsumeMessageBatchMaxSize(DEFAULT_BATCH_SIZE);\n\n        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {\n            if (CollectionUtils.isEmpty(msgs)) {\n                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;\n            }\n\n            if (msgs.size() != DEFAULT_BATCH_SIZE) {\n                LOG.error(\"MessageListenerConcurrently callback msgs.size() != 1\");\n            }\n\n            MessageExt msg = msgs.get(0);\n            if (msg.getReconsumeTimes() >= MAX_RETRY) {\n                LOG.error(\"reconsume exceed max retry times\");\n                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;\n            }\n\n            try {\n                if (process(objectMapper.readValue(new String(msg.getBody()), getClassT()))) {\n                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;\n                } else {\n                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;\n                }\n            } catch (Exception e) {\n                LOG.error(\"process exception\", e);\n                return ConsumeConcurrentlyStatus.RECONSUME_LATER;\n            }\n        });\n        try {\n            consumer.start();\n        } catch (MQClientException e) {\n            LOG.error(\"consumer start exception\", e);\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public void destroy() throws Exception {\n        consumer.shutdown();\n    }\n}\n```\n\n与Producer类似，topic、tag、namesrv由子类指定。\n\n- postConstruct：订阅了对应topic和tag的消息，并设定回掉函数，这里设定每批次最多拉取1个消息，以最简化处理失败的情况，你可以根据实际情况做出调整。\n\n- 接受消息时，会调用子类的process进行处理，同时进行json的反序列化操作\n\n接下来，我们来写一个Demo的生产者、消费者：\n\n首先配置nameSrv：\n\n```yaml\n# rocketmq\nrocketmq.namesrv: 127.0.0.1:9876\n```\n\n接着，定义消息：\n\n```java\n@Data\n@Builder\n@NoArgsConstructor\n@AllArgsConstructor\npublic class DemoMessage {\n\n    private String msg;\n\n    private long ts;\n}\n```\n\n然后是具体的Consumer和Producer：\n\n```java\npackage com.coder4.homs.demo.server.mq;\n\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\n/**\n * @author coder4\n */\n@Service\npublic class DemoConsumer extends BaseConsumer<DemoMessage> {\n\n    @Value(\"${rocketmq.namesrv}\")\n    private String namesrv;\n\n    @Override\n    String getNamesrvAddr() {\n        return namesrv;\n    }\n\n    @Override\n    String getConsumerGroup() {\n        return \"demo-consumer\";\n    }\n\n    @Override\n    String getTopic() {\n        return \"demo\";\n    }\n\n    @Override\n    String getTag() {\n        return \"*\";\n    }\n\n    @Override\n    Class<DemoMessage> getClassT() {\n        return DemoMessage.class;\n    }\n\n    @Override\n    boolean process(DemoMessage msg) {\n        LOG.info(\"process msg = {}\", msg);\n        return true;\n    }\n}\n```\n\n```java\npackage com.coder4.homs.demo.server.mq;\n\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\n/**\n * @author coder4\n */\n@Service\npublic class DemoProducer extends BaseProducer<DemoMessage> {\n\n    @Value(\"${rocketmq.namesrv}\")\n    private String namesrv;\n\n    @Override\n    String getNamesrvAddr() {\n        return namesrv;\n    }\n\n    @Override\n    String getProducerGroup() {\n        return \"demo-producer\";\n    }\n\n    @Override\n    String getTopic() {\n        return \"demo\";\n    }\n\n    @Override\n    String getTag() {\n        return \"*\";\n    }\n}\n```\n\n我们可以调用Producer发送一个消息，然后会收到如下的日志，说明消息已经被成功处理！\n\n```shell\n2021-10-12 8:01:37.340  INFO 6270 --- [MessageThread_1] c.c.homs.demo.server.mq.DemoConsumer     : process msg = DemoMessage(msg=123, ts=1634032897315)\n```\n\n由于篇幅所限，我们只实战了基础的消息收发，推荐你根据文档继续探索其他内容，包括：[集群部署]([Deployment - Apache RocketMQ](https://rocketmq.apache.org/docs/rmq-deployment/))、[顺序消息]([Order Message - Apache RocketMQ](https://rocketmq.apache.org/docs/order-example/))、[广播消息]([Broadcasting - Apache RocketMQ](https://rocketmq.apache.org/docs/broadcast-example/))等内容。\n"
  },
  {
    "path": "src/ch03-ms-dev2/registry1.md",
    "content": "# Nacos注册中心：注册篇\n\n![f](amazon-ms-structure.png)\n\n这是一张从互联网上找到的图，你的直观感受是什么？头皮发麻？\n\n实际上，这个球儿是某一年亚马逊的微服务结构图，每一个球的端点，都是一个微服务。\n\n假设某个微服务A，想通过RPC调用另一个微服务B，需要如何实现呢？\n\n1. 微服务B可能有多个实例，他需要先找到一个存活的实例，假设叫做B1。\n\n2. 需要知道B1的IP和端口\n\n3. 建立连接，发起请求，并响应结果。\n\n仔细揣摩上述流程，你会有一些疑问：\n\n1. 怎么知道B的哪个实例还在存活？\n\n2. 怎么知道B1的具体IP和端口？\n\n3. 假设微服务B扩容后，有一个新的B6，如何上服务A感知到呢？\n\n这些都是微服务注册中心要解决的问题。\n\n## Nacos服务注册中心\n\nNacos 致力于帮助您发现、配置和管理微服务。它提供了一组简单易用的特性集，帮助应用快速实现动态服务发现、服务配置、服务元数据及流量管理。\n\n为了演示基本原理，我们将采用单机模式，在实际生产环境中，建议你采用[集群部署](https://nacos.io/zh-cn/docs/cluster-mode-quick-start.html)。\n\n```bash\n#!/bin/bash\n\nNAME=\"nacos\"\nPUID=\"1000\"\nPGID=\"1000\"\n\n\ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    -e MODE=standalone \\\n    -p 8848:8848 \\\n    -p 9848:9848 \\\n    -p 9849:9849 \\\n    --detach \\\n    --restart always \\\n    nacos/nacos-server:2.0.3\n```\n\n如上，我们采用官方镜像的单机模式，端口介绍如下：\n\n- 8848是web界面和rest api端口\n\n- 9848、9849是gRPC端口\n\n启动成功后，访问http://127.0.0.1:8848，会进入如下界面：\n\n![f](./nacos-web.png)\n\n默认的用户名和密码都是nacos。\n\n## 服务端集成Nacos自动注册\n\n接下来，我们实现微服务的自动注册，即服务启动时，将自身的IP和端口，主动注册到Nacos上。\n\n由于我们的架构体系中，通过gRPC进行服务通信，因此我们只注册RPC的部分。我们沿用第2章中的设定，端口是5000。\n\n在服务端集成Nacos有很多方法，一般常见的都是直接使用spring-cloud-starter，但本书并没有采用这种做法，原因是：\n\n- 需要引入大量额外的cloud包，导致技术依赖过于旁杂。\n\n- cloud模式采用注解的方式，并不能很好支持\"一个微服务与多个不同微服务通信\"的场景。\n\n综上我们直接使用裸客户端的方式，首先是依赖：\n\n```groovy\nimplementation 'com.alibaba.nacos:nacos-client:2.0.3'\n```\n\n接着，我们在第2章的基础上，在RPC服务上做如下修改：\n\n```java\n@Configuration\npublic class RpcServerConfiguration {\n\n    private Logger LOG = LoggerFactory.getLogger(RpcServerConfiguration.class);\n\n    @Autowired\n    private BindableService bindableService;\n\n    @Autowired\n    private HomsRpcServer server;\n\n    @Autowired\n    private NacosService nacosService;\n\n    @Bean\n    public HomsRpcServer createRpcServer() {\n        return new HomsRpcServer(bindableService, 5000);\n    }\n\n    @PostConstruct\n    public void postConstruct() throws IOException, NacosException {\n        server.start();\n        // register\n        nacosService.registerRPC(SERVICE_NAME);\n    }\n\n    @PreDestroy\n    public void preDestory() throws NacosException {\n        try {\n            server.stop();\n        } catch (InterruptedException e) {\n            LOG.info(\"stop gRPC server exception\", e);\n        } finally {\n            // unregister\n            nacosService.deregisterRPC(SERVICE_NAME);\n            LOG.info(\"stop gRPC server done\");\n        }\n    }\n\n\n}\n```\n\n如上所示，我们在RPC服务启动的时候，增加了向Nacos的注册、在RPC停止的时候，在Nacos上注销服务。\n\nNacosService是对NacosClient的简单封装，代码如下：\n\n```java\n@Service\npublic class NacosServiceImpl implements NacosService {\n\n    @Value(\"${nacos.server}\")\n    private String nacosServer;\n\n    private NamingService namingService;\n\n    @PostConstruct\n    public void postConstruct() throws NacosException {\n        namingService = NamingFactory\n                .createNamingService(nacosServer);\n    }\n\n    @Override\n    public void registerRPC(String serviceName) throws NacosException {\n        namingService.registerInstance(serviceName, getIP(), 5000);\n    }\n\n    @Override\n    public void deregisterRPC(String serviceName) throws NacosException {\n        namingService.deregisterInstance(serviceName, getIP(), 5000);\n    }\n\n    private String getIP() {\n        return System.getProperty(\"POD_IP\", \"127.0.0.1\");\n    }\n}\n```\n\n如上所示，我们从yaml中读取Nacos服务的地址，然后从环境变量读取IP地址，并实现了注册、注销功能。\n\n这里，你可以暂时假定环境变量一定可以取到IP，在后续Kubernetes的章节，我们会介绍如何将Pod的IP注入容器的环境变量。\n\n你可以试着启动服务，然后访问Nacos的Web UI，会发现我们的服务正常发现了！\n\n至此，我们实现了服务端的服务注册。至于另一半，服务的发现，请听下回分解！\n"
  },
  {
    "path": "src/ch03-ms-dev2/registry2.md",
    "content": "# Nacos注册中心：发现篇\n\n经过上一节的努力，我们已经将RPC服务成功的注册到Nacos上了。\n\n我们还是以老生常谈的A调用B为例，B的所有实例B1、B2...都在Nacos上了。我们本节要实现的，都客户端，也就是A的部分。\n\n老规矩，先引入依赖：\n\n```groovy\nimplementation 'com.alibaba.nacos:nacos-client:2.0.3'\nimplementation 'org.springframework.boot:spring-boot-autoconfigure:2.2.0.RELEASE'\n```\n\n上述除了引入nacos的依赖外，还引入了spring-boot的自动配置包，后续做客户端的自动装配时会用到。\n\n## 客户端改造\n\n在正式对接Nacos前，我们先对客户端的包做一些改造。\n\n首先，引入一个通用的Grpc客户端实现：\n\n```java\npublic abstract class HSGrpcClient implements AutoCloseable {\n\n    private ManagedChannel channel;\n\n    private String ip;\n\n    private int port;\n\n    public HSGrpcClient(String ip, int port) {\n        this.ip = ip;\n        this.port = port;\n    }\n\n    public void init() {\n        channel = ManagedChannelBuilder\n                .forTarget(ip + \":\" + port)\n                .usePlaintext()\n                .build();\n        initSub(channel);\n    }\n\n    protected abstract void initSub(Channel channel);\n\n    public void close() throws InterruptedException {\n        channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);\n    }\n\n}\n```\n\n代码如上所示：\n\n- HSGrpcClient管理了ManagedChannel，这是用于实际网络通信的连接池。\n\n- 提供了initStub抽象方法，让子类根据自己的需求，去初始化自己的stub。\n\n- 实现了AutoCloseable接口，让客户端可以通过close方法自动关闭。\n\n在这个基础上，我们改造之前的具体RPC客户端，如下：\n\n```java\npublic class HomsDemoGrpcClient extends HSGrpcClient {\n\n    private Logger LOG = LoggerFactory.getLogger(HomsDemoGrpcClient.class);\n\n\n    private HomsDemoGrpc.HomsDemoFutureStub futureStub;\n\n    /**\n     * Construct client for accessing HelloWorld server using the existing channel.\n     */\n    public HomsDemoGrpcClient(String ip, int port) {\n        super(ip, port);\n    }\n\n    @Override\n    protected void initSub(Channel channel) {\n        futureStub = HomsDemoGrpc.newFutureStub(channel);\n    }\n\n    public Optional<Integer> add(int val1, int val2) {\n        AddRequest request = AddRequest.newBuilder().setVal1(val1).setVal2(val2).build();\n        try {\n\n            AddResponse response = futureStub.add(request).get();\n            return Optional.ofNullable(response.getVal());\n        } catch (Exception e) {\n            LOG.error(\"grpc add exception\", e);\n            return Optional.empty();\n        }\n    }\n\n}\n```\n\n如上，我们改用了FutureStub，并且将Manage的管理部分，移到了基类中。\n\n## SimpleGrpcClientManager的实现\n\n在正式引入Nacos之前，我们先实现一个“看起来没什么营养”的SimpleGrpcClientManager，它可以提供IP、Port直连的客户端管理。\n\n首先是基类：\n\n```java\npublic abstract class AbstractGrpcClientManager<T extends HSGrpcClient> {\n\n    protected Logger LOG = LoggerFactory.getLogger(getClass());\n\n    protected volatile CopyOnWriteArrayList<T> clientPools = new CopyOnWriteArrayList<>();\n\n    protected Class<T> kind;\n\n    public AbstractGrpcClientManager(Class<T> kind) {\n        this.kind = kind;\n    }\n\n    public Optional<T> getClient() {\n        if (clientPools.size() == 0) {\n            return Optional.empty();\n        }\n        int pos = ThreadLocalRandom.current().nextInt(clientPools.size());\n        return Optional.ofNullable(clientPools.get(pos));\n    }\n\n    public abstract void init() throws Exception;\n\n    public void shutdown() {\n        clientPools.forEach(c -> {\n            try {\n                shutdown(c);\n            } catch (InterruptedException e) {\n                LOG.error(\"shutdown client exception\", e);\n            }\n        });\n    }\n\n    protected void shutdown(HSGrpcClient client) throws InterruptedException {\n        client.close();\n    }\n\n    protected Optional<HSGrpcClient> buildHsGrpcClient(String ip, int port) {\n        try {\n            Class[] cArg = {String.class, int.class};\n            HSGrpcClient client = kind.getDeclaredConstructor(cArg)\n                    .newInstance(ip, port);\n            client.init();\n            return Optional.ofNullable(client);\n        } catch (Exception e) {\n            LOG.error(\"build MyGrpcClient exception, ip = \"+ ip + \" port = \"+ port, e);\n            return Optional.empty();\n        }\n    }\n\n}\n```\n\n代码如上，解释一下：\n\n- clientPools是一组HSGrpcClient对象，即支持同时与多个微服务实例(多组不同的ip和端口)建立连接。在微服务场景下，这一特性尤为重要。\n- 而从每一个HSGrpcClient的视角来看，其内置的ManagedChannel内部实现了连接池。因此针对同一个微服务的ip和端口，我们只需要一个HSGrpcClient的实例即可。\n\n下面，我们看一下基础的、不带服务发现的实现：\n\n```java\npackage com.coder4.homs.demo.client;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Arrays;\nimport java.util.concurrent.CopyOnWriteArrayList;\n\n/**\n * @author coder4\n */\npublic class SimpleGrpcClientManager<T extends HSGrpcClient> extends AbstractGrpcClientManager<T> {\n\n    protected Logger LOG = LoggerFactory.getLogger(SimpleGrpcClientManager.class);\n\n    private String ip;\n\n    private int port;\n\n    public SimpleGrpcClientManager(Class<T> kind, String ip, int port) {\n        super(kind);\n        this.ip = ip;\n        this.port = port;\n    }\n\n    public void init() {\n        // init one client only\n        HSGrpcClient client = buildHsGrpcClient(ip, port)\n                .orElseThrow(() -> new RuntimeException(\"build HsGrpcClient fail\"));\n        clientPools = new CopyOnWriteArrayList(Arrays.asList(client));\n    }\n\n    public static void main(String[] args) throws Exception {\n        SimpleGrpcClientManager<HomsDemoGrpcClient> manager = new SimpleGrpcClientManager(HomsDemoGrpcClient.class, \"127.0.0.1\", 5000);\n        manager.init();\n        manager.getClient().ifPresent(t -> System.out.println(t.add(1, 2)));\n        manager.shutdown();\n    }\n\n}\n```\n\n从上述实现中不难发现：\n\n- 该实现中，默认只与预先设定的IP和端口，构造一个单独的HSGrpcClient。\n\n- 由于IP和端口通过外部指定，因此使用了CopyOnWriteArrayList以保证线程安全。\n\n## NacosGrpcClientManager的实现\n\n下面，我们着手实现带Nacos服务发现的版本。\n\n```java\npackage com.coder4.homs.demo.client;\n\nimport com.alibaba.nacos.api.naming.NamingFactory;\nimport com.alibaba.nacos.api.naming.NamingService;\nimport com.alibaba.nacos.api.naming.listener.NamingEvent;\nimport com.alibaba.nacos.api.naming.pojo.Instance;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\n\n/**\n * @author coder4\n */\npublic class NacosGrpcClientManager<T extends HSGrpcClient> extends AbstractGrpcClientManager<T> {\n\n    protected String serviceName;\n\n    protected String nacosServer;\n\n    protected NamingService namingService;\n\n    public NacosGrpcClientManager(Class<T> kind, String nacosServer, String serviceName) {\n        super(kind);\n        this.nacosServer = nacosServer;\n        this.serviceName = serviceName;\n    }\n\n    @Override\n    public void init() throws Exception {\n        namingService = NamingFactory\n                .createNamingService(nacosServer);\n        namingService.subscribe(serviceName, e -> {\n            if (e instanceof NamingEvent) {\n                NamingEvent event = (NamingEvent) e;\n                rebuildClientPools(event.getInstances());\n            }\n        });\n        rebuildClientPools(namingService.selectInstances(serviceName, true));\n    }\n\n    private void rebuildClientPools(List<Instance> instanceList) {\n        ArrayList<HSGrpcClient> list = new ArrayList<>();\n        for (Instance instance : instanceList) {\n            buildHsGrpcClient(instance.getIp(), instance.getPort()).ifPresent(c -> list.add(c));\n        }\n        CopyOnWriteArrayList<T> oldClientPools = clientPools;\n        clientPools = new CopyOnWriteArrayList(list);\n        // destory old ones\n        oldClientPools.forEach(c -> {\n            try {\n                c.close();\n            } catch (InterruptedException e) {\n                LOG.error(\"MyGrpcClient shutdown exception\", e);\n            }\n        });\n    }\n\n}\n```\n\n解释如下：\n\n- 在init方法中，初始化了NamingService，并订阅对应serviceName服务的更新事件。\n\n- 当第一次，或者有服务更新时，我们会根据最新列表，重建所有的HSGrpcClient\n\n- 每次重建后，关闭老的HSGrpcClient\n\n为了让上述客户端使用更加方便，我们添加了如下的自动配置：\n\n```java\n@Configuration\npublic class HomsDemoGrpcClientManagerConfiguration {\n\n    @Bean(name = \"homsDemoGrpcClientManager\")\n    @ConditionalOnMissingBean(name = \"homsDemoGrpcClientManager\")\n    @ConditionalOnProperty(name = {\"nacos.server\"})\n    public AbstractGrpcClientManager<HomsDemoGrpcClient> nacosManager(\n            @Value(\"${nacos.server}\") String nacosServer) throws Exception {\n        NacosGrpcClientManager<HomsDemoGrpcClient> manager =\n                new NacosGrpcClientManager<>(HomsDemoGrpcClient.class,\n                        nacosServer, HomsDemoConstant.SERVICE_NAME);\n        manager.init();\n        return manager;\n    }\n}\n```\n\n如上所示：\n\n- nacos的server地址由yaml中配置\n\n- serviceName由client包中的常量文件HomsDemoConstant提供(即homs-demo)\n\n为了让上述自动配置自动生效，我们还需要添加META-INF/spring.factories文件\n\n```ini\norg.springframework.boot.autoconfigure.EnableAutoConfiguration=\\\ncom.coder4.homs.demo.configuration.HomsDemoGrpcClientManagerConfiguration\n```\n\n最后，我们来实验一下服务发现的效果\n\n1. 启动Server进程，检查Nacos上，应当出现了自动注册的RPC服务。\n\n2. 开发客户端驱动的项目，引用上述client包、配置yaml中的nacos服务地址\n\n3. 最后，在客户端驱动项目中，通过Autowired自动装配，代码类似：\n\n```java\n@Autowired\nprivate AbstractGrpcClientManager<HomsDemoGrpcClient> homsClientManager;\n\n\n// Usage\nhomsClientManager.getClient().ifPresent(client -> client.add(1, 2));\n```\n\n如果一切顺利，会自动发现nacos上已经注册的服务实例，并成功执行rpc调用。\n"
  },
  {
    "path": "src/ch04-ms-dev3/README.md",
    "content": "## 微服务开发下篇：日志、链路追踪、监控\n\n随着微服务架构的流行，可观测性(Observability)的理念也逐渐升温。\n\n可观测性是一个源于控制论的概念，映射到微服务架构中，主要指三个方面：\n\n- 日志：微服务的进程产生日志，分散在各处，系统需要收集、归拢日志，并提供统一的日志查询、分析功能。\n\n- 链路追踪：微服务调用关系错综复杂，如果某一个微服务发生故障，很有可能是来源上有的调用挂掉。通过链路追踪，可以轻松的定位和发现问题。\n\n- 监控：监控系统收集物理机、微服务的各类指标(Metrics)，从而反应系统运行情况。更进一步，可以通过图表的方式，可视化地展示需求。\n\n本章，我们将围绕上述三点展开：\n\n- 基于ElasticSearch + FileBeats + Kafka + FileBeats + Kibana的日志平台\n\n- 基于SkyWalking的链路追踪系统\n\n- 基于VictorialMetrics + Grafana的监控系统\n\n经过本章的实战，微服务架构的可观测性将得到明显提升。\n"
  },
  {
    "path": "src/ch04-ms-dev3/elkfk.md",
    "content": "# 基于ELKFK打造日志平台\n\n微服务的实例数众多，需要一个强大的日志日志平台，它应具有以下功能：\n\n- 采集：从服务端进程(k8s的Pod中)，自动收集日志\n\n- 存储：将日志按照时间序列，存储在持久化的介质上，以供未来查找。\n\n- 检索：根据关键词，时间等条件，方便地检索特定日志内容。\n\n我们将基于ELKFK，打造自己的日志平台。\n\n你可能听说过ELK，那么ELK后面加上的FK是什么呢？\n\nF：Filebeat，轻量级的日志采集插件\n\nK：Kafka，用户缓存日志\n\n日志系统的架构图如下所示：\n\n![f](./elkfk.png)\n\n## 搭建Kafka\n\nKafka消耗的资源较多，一般多采用独立部署的方式。\n\n这里为了演示方便，我们以单机版为例。\n\n首先下载：\n\n```shell\nwget https://dlcdn.apache.org/kafka/3.0.0/kafka_2.13-3.0.0.tgz\n```\n\n接着，启动zk\n\n```shell\nbin/zookeeper-server-start.sh config/zookeeper.properties\n```\n\n最后，启动broker\n\n```shell\nbin/kafka-server-start.sh config/server.properties\n```\n\n我们来创建topic，供后续使用。\n\n```shell\nbin/kafka-topics.sh --create --topic k8s-log-homs --partitions 3 --replication-factor 1 --bootstrap-server localhost:9092k8s -> (FileBeat) -> kafka\n```\n\n## 部署FileBeat\n\n有了Kafka之后，我们在Kubernets集群上部署FileBeat，自动采集日志并发送到Kafka的队列中，配置如下：\n\n```yaml\n---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: filebeat-config\n  namespace: kube-system\n  labels:\n    k8s-app: filebeat\ndata:\n  filebeat.yml: |-\n    filebeat.inputs:\n    - type: container\n      paths:\n        - /var/log/containers/homs*.log\n      fields:\n        kafka_topic: k8s-log-homs\n      processors:\n        - add_kubernetes_metadata:\n            host: ${NODE_NAME}\n            matchers:\n            - logs_path:\n                logs_path: \"/var/log/containers/\"\n    processors:\n      - add_cloud_metadata:\n      - add_host_metadata:\n\n    cloud.id: ${ELASTIC_CLOUD_ID}\n    cloud.auth: ${ELASTIC_CLOUD_AUTH}\n    output:\n      kafka:\n        enabled: true\n        hosts: [\"10.1.172.136:9092\"]\n        topic: '%{[fields.kafka_topic]}' \n        max_message_bytes: 5242880\n        partition.round_robin:\n          reachable_only: true\n        keep-alive: 120\n        required_acks: 1\n---\napiVersion: apps/v1\nkind: DaemonSet\nmetadata:\n  name: filebeat\n  namespace: kube-system\n  labels:\n    k8s-app: filebeat\nspec:\n  selector:\n    matchLabels:\n      k8s-app: filebeat\n  template:\n    metadata:\n      labels:\n        k8s-app: filebeat\n    spec:\n      serviceAccountName: filebeat\n      terminationGracePeriodSeconds: 30\n      hostNetwork: true\n      dnsPolicy: ClusterFirstWithHostNet\n      containers:\n      - name: filebeat\n        image: docker.elastic.co/beats/filebeat:7.15.2\n        args: [\n          \"-c\", \"/etc/filebeat.yml\",\n          \"-e\",\n        ]\n        env:\n        - name: ELASTIC_CLOUD_ID\n          value:\n        - name: ELASTIC_CLOUD_AUTH\n          value:\n        - name: NODE_NAME\n          valueFrom:\n            fieldRef:\n              fieldPath: spec.nodeName\n        securityContext:\n          runAsUser: 0\n          # If using Red Hat OpenShift uncomment this:\n          #privileged: true\n        resources:\n          limits:\n            memory: 200Mi\n          requests:\n            cpu: 100m\n            memory: 100Mi\n        volumeMounts:\n        - name: config\n          mountPath: /etc/filebeat.yml\n          readOnly: true\n          subPath: filebeat.yml\n        - name: data\n          mountPath: /usr/share/filebeat/data\n        - name: varlibdockercontainers\n          mountPath: /var/lib/docker/containers\n          readOnly: true\n        - name: varlog\n          mountPath: /var/log\n          readOnly: true\n      volumes:\n      - name: config\n        configMap:\n          defaultMode: 0640\n          name: filebeat-config\n      - name: varlibdockercontainers\n        hostPath:\n          path: /var/lib/docker/containers\n      - name: varlog\n        hostPath:\n          path: /var/log\n      # data folder stores a registry of read status for all files, so we don't send everything again on a Filebeat pod restart\n      - name: data\n        hostPath:\n          # When filebeat runs as non-root user, this directory needs to be writable by group (g+w).\n          path: /var/lib/filebeat-data\n          type: DirectoryOrCreate\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n  name: filebeat\nsubjects:\n- kind: ServiceAccount\n  name: filebeat\n  namespace: kube-system\nroleRef:\n  kind: ClusterRole\n  name: filebeat\n  apiGroup: rbac.authorization.k8s.io\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: RoleBinding\nmetadata:\n  name: filebeat\n  namespace: kube-system\nsubjects:\n  - kind: ServiceAccount\n    name: filebeat\n    namespace: kube-system\nroleRef:\n  kind: Role\n  name: filebeat\n  apiGroup: rbac.authorization.k8s.io\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: RoleBinding\nmetadata:\n  name: filebeat-kubeadm-config\n  namespace: kube-system\nsubjects:\n  - kind: ServiceAccount\n    name: filebeat\n    namespace: kube-system\nroleRef:\n  kind: Role\n  name: filebeat-kubeadm-config\n  apiGroup: rbac.authorization.k8s.io\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n  name: filebeat\n  labels:\n    k8s-app: filebeat\nrules:\n- apiGroups: [\"\"] # \"\" indicates the core API group\n  resources:\n  - namespaces\n  - pods\n  - nodes\n  verbs:\n  - get\n  - watch\n  - list\n- apiGroups: [\"apps\"]\n  resources:\n    - replicasets\n  verbs: [\"get\", \"list\", \"watch\"]\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: Role\nmetadata:\n  name: filebeat\n  # should be the namespace where filebeat is running\n  namespace: kube-system\n  labels:\n    k8s-app: filebeat\nrules:\n  - apiGroups:\n      - coordination.k8s.io\n    resources:\n      - leases\n    verbs: [\"get\", \"create\", \"update\"]\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: Role\nmetadata:\n  name: filebeat-kubeadm-config\n  namespace: kube-system\n  labels:\n    k8s-app: filebeat\nrules:\n  - apiGroups: [\"\"]\n    resources:\n      - configmaps\n    resourceNames:\n      - kubeadm-config\n    verbs: [\"get\"]\n---\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: filebeat\n  namespace: kube-system\n  labels:\n    k8s-app: filebeat\n---\n```\n\n配置较多，我们解释一下：\n\n- 采集/var/log/containers目录下的homs*.log文件名的日志\n\n- 将这些日志送到k8s-log-homs这个Kafka的topic中\n\n- 配置Kafka的服务器地址\n\n- 配置其他所需的权限\n\n实际上，上述配置是在官方[原始文件](wget https://raw.githubusercontent.com/elastic/beats/7.15/deploy/kubernetes/filebeat-kubernetes.yaml)基础上修改的，更多配置可以参考[官方文档]([Configure the Kafka output | Filebeat Reference [7.15] | Elastic](https://www.elastic.co/guide/en/beats/filebeat/current/kafka-output.html))。\n\n应用上述配置：\n\n```shell\nkubectl apply -f filebeat.yaml\n```\n\n然后我们查看Kafka收到的日志：\n\n```shell\nbin/kafka-console-consumer.sh --bootstrap-server 127.0.0.1:9092 --topic k8s-log-homs --from-beginning\n```\n\n符合预期：\n\n```shell\n...\n{\"@timestamp\":\"2021-11-15T03:18:26.487Z\",\"@metadata\":{\"beat\":\"filebeat\",\"type\":\"_doc\",\"version\":\"7.15.2\"},\"stream\":\"stdout\",\"message\":\"2021-11-15 03:18:26.486  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)\",\"log\":{\"offset\":1491,\"file\":{\"path\":\"/var/log/containers/homs-start-deployment-6878f48fcc-65vcr_default_homs-start-server-d37b0467d097c00bd203089a97df371cdbacc156493f6b2d995b80395caf516f.log\"}},\"input\":{\"type\":\"container\"},\"agent\":{\"type\":\"filebeat\",\"version\":\"7.15.2\",\"hostname\":\"minikube\",\"ephemeral_id\":\"335988de-a165-4070-88f1-08c3d6be7ba5\",\"id\":\"850b6889-85e0-41c5-8a83-bce344b8b2ec\",\"name\":\"minikube\"},\"ecs\":{\"version\":\"1.11.0\"},\"container\":{\"image\":{\"name\":\"coder4/homs-start:107\"},\"id\":\"d37b0467d097c00bd203089a97df371cdbacc156493f6b2d995b80395caf516f\",\"runtime\":\"docker\"},\"kubernetes\":{\"pod\":{\"ip\":\"172.17.0.3\",\"name\":\"homs-start-deployment-6878f48fcc-65vcr\",\"uid\":\"7d925249-2a77-4c28-a462-001d189cdeaa\"},\"container\":{\"name\":\"homs-start-server\"},\"node\":{\"name\":\"minikube\",\"uid\":\"faec4c1a-9188-408a-aeec-95b24aa47a88\",\"labels\":{\"node-role_kubernetes_io/control-plane\":\"\",\"node_kubernetes_io/exclude-from-external-load-balancers\":\"\",\"kubernetes_io/hostname\":\"minikube\",\"kubernetes_io/os\":\"linux\",\"minikube_k8s_io/commit\":\"a03fbcf166e6f74ef224d4a63be4277d017bb62e\",\"minikube_k8s_io/name\":\"minikube\",\"minikube_k8s_io/updated_at\":\"2021_11_05T12_15_23_0700\",\"node-role_kubernetes_io/master\":\"\",\"beta_kubernetes_io/arch\":\"amd64\",\"beta_kubernetes_io/os\":\"linux\",\"minikube_k8s_io/version\":\"v1.22.0\",\"kubernetes_io/arch\":\"amd64\"},\"hostname\":\"minikube\"},\"labels\":{\"app\":\"homs-start\",\"pod-template-hash\":\"6878f48fcc\"},\"namespace_uid\":\"b880885d-c94a-4cf2-ba2c-1e4cb0d1a691\",\"namespace_labels\":{\"kubernetes_io/metadata_name\":\"default\"},\"namespace\":\"default\",\"deployment\":{\"name\":\"homs-start-deployment\"},\"replicaset\":{\"name\":\"homs-start-deployment-6878f48fcc\"}},\"orchestrator\":{\"cluster\":{\"url\":\"control-plane.minikube.internal:8443\",\"name\":\"mk\"}},\"host\":{\"mac\":[\"02:42:d5:27:3f:31\",\"c6:64:9d:f9:89:7b\",\"5a:b1:a0:66:ee:d3\",\"46:41:6e:14:85:14\",\"02:42:c0:a8:31:02\"],\"hostname\":\"minikube\",\"architecture\":\"x86_64\",\"os\":{\"kernel\":\"5.10.47-linuxkit\",\"codename\":\"Core\",\"type\":\"linux\",\"platform\":\"centos\",\"version\":\"7 (Core)\",\"family\":\"redhat\",\"name\":\"CentOS Linux\"},\"id\":\"1820c6c61258c329e88764d3dc4484f3\",\"name\":\"minikube\",\"containerized\":true,\"ip\":[\"172.17.0.1\",\"192.168.49.2\"]}}\n{\"@timestamp\":\"2021-11-15T03:18:26.573Z\",\"@metadata\":{\"beat\":\"filebeat\",\"type\":\"_doc\",\"version\":\"7.15.2\"},\"log\":{\"offset\":2111,\"file\":{\"path\":\"/var/log/containers/homs-start-deployment-6878f48fcc-65vcr_default_homs-start-server-d37b0467d097c00bd203089a97df371cdbacc156493f6b2d995b80395caf516f.log\"}},\"stream\":\"stdout\",\"input\":{\"type\":\"container\"},\"host\":{\"id\":\"1820c6c61258c329e88764d3dc4484f3\",\"containerized\":true,\"ip\":[\"172.17.0.1\",\"192.168.49.2\"],\"name\":\"minikube\",\"mac\":[\"02:42:d5:27:3f:31\",\"c6:64:9d:f9:89:7b\",\"5a:b1:a0:66:ee:d3\",\"46:41:6e:14:85:14\",\"02:42:c0:a8:31:02\"],\"hostname\":\"minikube\",\"architecture\":\"x86_64\",\"os\":{\"family\":\"redhat\",\"name\":\"CentOS Linux\",\"kernel\":\"5.10.47-linuxkit\",\"codename\":\"Core\",\"type\":\"linux\",\"platform\":\"centos\",\"version\":\"7 (Core)\"}},\"ecs\":{\"version\":\"1.11.0\"},\"agent\":{\"version\":\"7.15.2\",\"hostname\":\"minikube\",\"ephemeral_id\":\"335988de-a165-4070-88f1-08c3d6be7ba5\",\"id\":\"850b6889-85e0-41c5-8a83-bce344b8b2ec\",\"name\":\"minikube\",\"type\":\"filebeat\"},\"message\":\"2021-11-15 03:18:26.573  INFO 1 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext\",\"container\":{\"id\":\"d37b0467d097c00bd203089a97df371cdbacc156493f6b2d995b80395caf516f\",\"runtime\":\"docker\",\"image\":{\"name\":\"coder4/homs-start:107\"}},\"kubernetes\":{\"replicaset\":{\"name\":\"homs-start-deployment-6878f48fcc\"},\"node\":{\"name\":\"minikube\",\"uid\":\"faec4c1a-9188-408a-aeec-95b24aa47a88\",\"labels\":{\"node-role_kubernetes_io/control-plane\":\"\",\"minikube_k8s_io/commit\":\"a03fbcf166e6f74ef224d4a63be4277d017bb62e\",\"kubernetes_io/os\":\"linux\",\"kubernetes_io/arch\":\"amd64\",\"node_kubernetes_io/exclude-from-external-load-balancers\":\"\",\"beta_kubernetes_io/arch\":\"amd64\",\"beta_kubernetes_io/os\":\"linux\",\"minikube_k8s_io/updated_at\":\"2021_11_05T12_15_23_0700\",\"node-role_kubernetes_io/master\":\"\",\"kubernetes_io/hostname\":\"minikube\",\"minikube_k8s_io/name\":\"minikube\",\"minikube_k8s_io/version\":\"v1.22.0\"},\"hostname\":\"minikube\"},\"namespace_labels\":{\"kubernetes_io/metadata_name\":\"default\"},\"namespace\":\"default\",\"deployment\":{\"name\":\"homs-start-deployment\"},\"pod\":{\"ip\":\"172.17.0.3\",\"name\":\"homs-start-deployment-6878f48fcc-65vcr\",\"uid\":\"7d925249-2a77-4c28-a462-001d189cdeaa\"},\"labels\":{\"app\":\"homs-start\",\"pod-template-hash\":\"6878f48fcc\"},\"container\":{\"name\":\"homs-start-server\"},\"namespace_uid\":\"b880885d-c94a-4cf2-ba2c-1e4cb0d1a691\"},\"orchestrator\":{\"cluster\":{\"url\":\"control-plane.minikube.internal:8443\",\"name\":\"mk\"}}}\n{\"@timestamp\":\"2021-11-15T03:18:27.470Z\",\"@metadata\":{\"beat\":\"filebeat\",\"type\":\"_doc\",\"version\":\"7.15.2\"},\"input\":{\"type\":\"container\"},\"orchestrator\":{\"cluster\":{\"url\":\"control-plane.minikube.internal:8443\",\"name\":\"mk\"}},\"agent\":{\"type\":\"filebeat\",\"version\":\"7.15.2\",\"hostname\":\"minikube\",\"ephemeral_id\":\"335988de-a165-4070-88f1-08c3d6be7ba5\",\"id\":\"850b6889-85e0-41c5-8a83-bce344b8b2ec\",\"name\":\"minikube\"},\"stream\":\"stdout\",\"message\":\"2021-11-15 03:18:27.470  INFO 1 --- [           main] com.homs.start.StartApplication          : Started StartApplication in 3.268 seconds (JVM running for 3.738)\",\"kubernetes\":{\"pod\":{\"name\":\"homs-start-deployment-6878f48fcc-65vcr\",\"uid\":\"7d925249-2a77-4c28-a462-001d189cdeaa\",\"ip\":\"172.17.0.3\"},\"container\":{\"name\":\"homs-start-server\"},\"labels\":{\"app\":\"homs-start\",\"pod-template-hash\":\"6878f48fcc\"},\"node\":{\"labels\":{\"kubernetes_io/arch\":\"amd64\",\"node_kubernetes_io/exclude-from-external-load-balancers\":\"\",\"beta_kubernetes_io/arch\":\"amd64\",\"kubernetes_io/hostname\":\"minikube\",\"minikube_k8s_io/name\":\"minikube\",\"minikube_k8s_io/version\":\"v1.22.0\",\"kubernetes_io/os\":\"linux\",\"minikube_k8s_io/commit\":\"a03fbcf166e6f74ef224d4a63be4277d017bb62e\",\"minikube_k8s_io/updated_at\":\"2021_11_05T12_15_23_0700\",\"node-role_kubernetes_io/control-plane\":\"\",\"node-role_kubernetes_io/master\":\"\",\"beta_kubernetes_io/os\":\"linux\"},\"hostname\":\"minikube\",\"name\":\"minikube\",\"uid\":\"faec4c1a-9188-408a-aeec-95b24aa47a88\"},\"namespace\":\"default\",\"deployment\":{\"name\":\"homs-start-deployment\"},\"namespace_uid\":\"b880885d-c94a-4cf2-ba2c-1e4cb0d1a691\",\"namespace_labels\":{\"kubernetes_io/metadata_name\":\"default\"},\"replicaset\":{\"name\":\"homs-start-deployment-6878f48fcc\"}},\"ecs\":{\"version\":\"1.11.0\"},\"host\":{\"os\":{\"codename\":\"Core\",\"type\":\"linux\",\"platform\":\"centos\",\"version\":\"7 (Core)\",\"family\":\"redhat\",\"name\":\"CentOS Linux\",\"kernel\":\"5.10.47-linuxkit\"},\"id\":\"1820c6c61258c329e88764d3dc4484f3\",\"containerized\":true,\"ip\":[\"172.17.0.1\",\"192.168.49.2\"],\"mac\":[\"02:42:d5:27:3f:31\",\"c6:64:9d:f9:89:7b\",\"5a:b1:a0:66:ee:d3\",\"46:41:6e:14:85:14\",\"02:42:c0:a8:31:02\"],\"hostname\":\"minikube\",\"architecture\":\"x86_64\",\"name\":\"minikube\"},\"log\":{\"offset\":2787,\"file\":{\"path\":\"/var/log/containers/homs-start-deployment-6878f48fcc-65vcr_default_homs-start-server-d37b0467d097c00bd203089a97df371cdbacc156493f6b2d995b80395caf516f.log\"}},\"container\":{\"image\":{\"name\":\"coder4/homs-start:107\"},\"id\":\"d37b0467d097c00bd203089a97df371cdbacc156493f6b2d995b80395caf516f\",\"runtime\":\"docker\"}}\n```\n\n重启deployment\n\n```shell\nkubectl rollout restart deployment homs-start-deployment\n```\n\n启动ElasticSearch\n\n```shell\n#!/bin/bash\n\nNAME=\"elasticsearch\"\nPUID=\"1000\"\nPGID=\"1000\"\n\nVOLUME=\"$HOME/docker_data/elasticsearch\"\nmkdir -p $VOLUME \n\ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    --env discovery.type=single-node \\\n    -p 9200:9200 \\\n    -p 9300:9300 \\\n    --detach \\\n    --restart always \\\n    docker.elastic.co/elasticsearch/elasticsearch:7.15.2\n```\n\n## 启动ElasticSearch\n\n在配置LogStash前，我们先要启动最终的存储，即ElasticSearch。\n\n为了演示方便，我们使用单机模式启动：\n\n```shell\n#!/bin/bash\n\nNAME=\"elasticsearch\"\nPUID=\"1000\"\nPGID=\"1000\"\n\nVOLUME=\"$HOME/docker_data/elasticsearch\"\nmkdir -p $VOLUME \n\ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    --env discovery.type=single-node \\\n    -p 9200:9200 \\\n    -p 9300:9300 \\\n    --detach \\\n    --restart always \\\n    docker.elastic.co/elasticsearch/elasticsearch:7.15.2\n```\n\n你可以通过curl命令，检查启动是否成功：\n\n```shell\ncurl 127.0.0.1:9200 \n\n{\n  \"name\" : \"elasticsearch\",\n  \"cluster_name\" : \"docker-cluster\",\n  \"cluster_uuid\" : \"yxLELfOmT9OXPXxjh7g7Nw\",\n  \"version\" : {\n    \"number\" : \"7.15.2\",\n    \"build_flavor\" : \"default\",\n    \"build_type\" : \"docker\",\n    \"build_hash\" : \"93d5a7f6192e8a1a12e154a2b81bf6fa7309da0c\",\n    \"build_date\" : \"2021-11-04T14:04:42.515624022Z\",\n    \"build_snapshot\" : false,\n    \"lucene_version\" : \"8.9.0\",\n    \"minimum_wire_compatibility_version\" : \"6.8.0\",\n    \"minimum_index_compatibility_version\" : \"6.0.0-beta1\"\n  },\n  \"tagline\" : \"You Know, for Search\"\n```\n\n温馨提示：默认情况是没有用户名、密码的，用于生产环境时请务必开启。\n\n## 启动Logstash\n\n首先，配置logstash.conf，将其放到pipeline子目录下：\n\n```ini\ninput {\n  kafka {\n    bootstrap_servers => [\"10.1.172.136:9092\"]\n    group_id => \"k8s-log-homs-logstash\"\n    topics => [\"k8s-log-homs\"] \n    codec => json\n }\n}\nfilter {\n  if [message] =~ \"\\tat\" {\n    grok {\n      match => [\"message\", \"^(\\tat)\"]\n      add_tag => [\"stacktrace\"]\n    }\n  }\n\n  grok {\n    match => [ \"message\",\n               \"%{TIMESTAMP_ISO8601:logtime}%{SPACE}%{LOGLEVEL:level}%{SPACE}(?<logmessage>.*)\"\n             ]\n  }\n\n  date {\n    match => [ \"logtime\" , \"yyyy-MM-dd HH:mm:ss.SSS\" ]\n  }\n\n  #mutate {\n  #  remove_field => [\"message\"]\n  #}\n}\n\noutput {\n  elasticsearch {\n    hosts => \"http://10.1.172.136:9200\"\n    user =>\"elastic\"\n    password =>\"\"\n    index => \"k8s-log-homs-%{+YYYY.MM.dd}\"\n }\n}\n```\n\n这里，我们使用了grok来拆分message字段，你可以在使用[在线工具]([Test grok patterns](https://grokconstructor.appspot.com/do/match))验证规则。\n\n接着，我们启动logstash\n\n```shell\n#!/bin/bash\n\nNAME=\"logstash\"\nPUID=\"1000\"\nPGID=\"1000\"\n\nVOLUME=\"$(pwd)/pipeline\"\n\ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    --volume \"$VOLUME\":/usr/share/logstash/pipeline \\\n    --detach \\\n    --restart always \\\n    docker.elastic.co/logstash/logstash:7.15.2\n```\n\n上述直接挂载了前面配置的pipeline目录。\n\n## Kibana\n\n最后，我们启动kibana：\n\n```shell\n#!/bin/bash\n\nNAME=\"kibana\"\nPUID=\"1000\"\nPGID=\"1000\"\n\ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    --env \"ELASTICSEARCH_HOSTS=http://10.1.172.136:9200\" \\\n    -p 5601:5601 \\\n    --detach \\\n    --restart always \\\n    docker.elastic.co/kibana/kibana:7.15.2\n```\n\n如果一切顺利，你会看到如图所示的日志：\n\n![f](./kibana-log.png)\n\n至此，我们已经成功搭建了自己的日志平台。\n"
  },
  {
    "path": "src/ch04-ms-dev3/micrometer.md",
    "content": "# 基于MicroMeter实现应用监控指标\n\n提到“监控”(Moniter)，你的第一反应是什么？\n\n是老传统监控软件Zabbix、Nagios？还是近几年火爆IT圈的Promethos？\n\n别急着比较系统，这篇文章，我们先聊聊应用监控指标。 \n\n顾名思义，“应用监指标”就是根据监控需求，在我们的应用系统中预设埋点，并支持与监控系统对接。\n\n典型的监控项如：接口请求次数、接口响应时间、接口报错次数....\n\n我们将介绍MicroMeter开源项目，并使用它实现Spring MVC的应用监控指标。\n\n## MicroMeter简介\n\nMicrometer是社区最流行的监控项项目之一，它提供了一个抽象、强大、易用的抽象门面接口，可以轻松的对接包括Prometheus、JMX等在内的近20种监控系统。它的作用和Slf4j类似，只不过它关注的不是日志，而是应用指标(application metrics）。\n\n## 自定义应用监控项初探\n\n下面，我们来开始micrometer之旅。\n\n由于网上关于micrometer对接Prometheus的文章已经很多了，这里我特意选择了JMX。\n\n通过JMX Bean暴露的监控项，你可以轻松的对接Zabbix等老牌监控系统。\n\n这里提醒的是JMX不支持类似Prometheus的层级结构，而只支持一级结构(tag会被打平)，具体可以参见[官方文档](https://micrometer.io/docs/registry/jmx)。当然，这在代码实现上是完全透明的。\n\n首先，我们新建一个简单的Spring Boot项目，并引入pom文件：\n\n```xml\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-actuator</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>io.micrometer</groupId>\n            <artifactId>micrometer-registry-jmx</artifactId>\n            <version>1.8.7</version>\n        </dependency>\n```\n\n然后开发如下的Spring MVC接口：\n\n```java\npackage com.coder4.homs.micrometer.web;\n\n\nimport com.coder4.homs.micrometer.web.data.UserVO;\nimport io.micrometer.core.instrument.Counter;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.RestController;\n\nimport javax.annotation.PostConstruct;\n\n@RestController\npublic class UserController {\n\n    @Autowired\n    private MeterRegistry meterRegistry;\n\n    private Counter COUNTER_GET_USER;\n\n    @PostConstruct\n    public void init() {\n        COUNTER_GET_USER = meterRegistry.counter(\"app_requests_method_count\", \"method\", \"UserController.getUser\");\n    }\n\n    @GetMapping(path = \"/users/{id}\")\n    public UserVO getUser(@PathVariable int id) {\n        UserVO user = new UserVO();\n        user.setId(id);\n        user.setName(String.format(\"user_%d\", id));\n\n        COUNTER_GET_USER.increment();\n        return user;\n    }\n\n}\n```\n\n在上面的代码中：\n\n1. 我们实现了UserController这个REST接口，他之中的/users/{id}可以获取用户。\n\n2. UserController注册了一个Counter，Counter由名字和tag组成，用过Prometheus的应该很熟悉这种套路了。\n\n3. 每次请求时，会将上述Counter加一操作。\n\n我们来测试一下，执行2次\n\n```shell\ncurl \"127.0.0.1:8080/users/1\"\n{\"id\":1,\"name\":\"user_1\"}\n```\n\n然后打开本地的jconsole，可以发现JMX Bean暴露出了了metrics、gauge等分类，我们打开\"metrics/app_requests_method_...\"这个指标，点击进去，可以发现具体的值也就是2。\n\n![f](./jconsole1.png)\n\n## 借助拦截器批量统计监控项目\n\n上述代码可以实现功能，但是你应该发现了，实现起来很繁琐，如果我们有10个接口，那岂不是要写很多无用代码？\n\n相信你已经想到了，可以用类似AOP (切面编程)的思路，解决问题。\n\n不过针对Spring MVC这个场景，使用AOP有点“大炮打蚊子”的感觉，我们可以使用拦截器实现。\n\n首先自定义拦截器的自动装配：\n\n```java\npackage com.coder4.homs.micrometer.configure;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\n@Configuration\npublic class MeterConfig implements WebMvcConfigurer {\n\n    @Bean\n    public MeterInterceptor getMeterInterceptor() {\n        return new MeterInterceptor();\n    }\n\n    @Override\n    public void addInterceptors(InterceptorRegistry registry){\n        registry.addInterceptor(getMeterInterceptor())\n                .addPathPatterns(\"/**\")\n                .excludePathPatterns(\"/error\")\n                .excludePathPatterns(\"/static/*\");\n    }\n}\n```\n\n上面代码很简单，就是新增了新的拦截器MeterInterceptor。\n\n我们看下拦截器做了什么：\n\n```java\npackage com.coder4.homs.micrometer.configure;\n\nimport io.micrometer.core.instrument.Counter;\nimport io.micrometer.core.instrument.DistributionSummary;\nimport io.micrometer.core.instrument.MeterRegistry;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.web.method.HandlerMethod;\nimport org.springframework.web.servlet.HandlerInterceptor;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.util.Optional;\n\npublic class MeterInterceptor implements HandlerInterceptor {\n\n    @Autowired\n    private MeterRegistry meterRegistry;\n\n    private ThreadLocal<Long> tlTimer = new ThreadLocal<>();\n\n    private static Optional<String> getMethod(HttpServletRequest request, Object handler) {\n        if (handler instanceof HandlerMethod) {\n            return Optional.of(String.format(\"%s_%s_%s\", ((HandlerMethod) handler).getBeanType().getSimpleName(),\n                    ((HandlerMethod) handler).getMethod().getName(), request.getMethod()));\n        } else {\n            return Optional.empty();\n        }\n    }\n\n    private void recordTimeDistribution(HttpServletRequest request, Object handler, long ms) {\n        Optional<String> methodOp = getMethod(request, handler);\n        if (methodOp.isPresent()) {\n            DistributionSummary.builder(\"app_requests_time_ms\")\n                    .tag(\"method\", methodOp.get())\n                    .publishPercentileHistogram()\n                    .register(meterRegistry)\n                    .record(ms);\n        }\n    }\n\n    public Optional<Counter> getCounterOfTotalCounts(HttpServletRequest request, Object handler) {\n        Optional<String> methodOp = getMethod(request, handler);\n        if (methodOp.isPresent()) {\n            return Optional.of(meterRegistry.counter(\"app_requests_total_counts\", \"method\",\n                    methodOp.get()));\n        } else {\n            return Optional.empty();\n        }\n    }\n\n    public Optional<Counter> getCounterOfExceptionCounts(HttpServletRequest request, Object handler) {\n        Optional<String> methodOp = getMethod(request, handler);\n        if (methodOp.isPresent()) {\n            return Optional.of(meterRegistry.counter(\"app_requests_exption_counts\", \"method\",\n                    methodOp.get()));\n        } else {\n            return Optional.empty();\n        }\n    }\n\n    public Optional<Counter> getCounterOfRespCodeCounts(HttpServletRequest request, HttpServletResponse response,\n                                                        Object handler) {\n        Optional<String> methodOp = getMethod(request, handler);\n        if (methodOp.isPresent()) {\n            return Optional.of(meterRegistry.counter(String.format(\"app_requests_resp%d_counts\", response.getStatus()),\n                    \"method\", methodOp.get()));\n        } else {\n            return Optional.empty();\n        }\n    }\n\n    @Override\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\n        tlTimer.set(System.currentTimeMillis());\n        return true;\n    }\n\n    @Override\n    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {\n        // record time\n        recordTimeDistribution(request, handler, System.currentTimeMillis() - tlTimer.get());\n        tlTimer.remove();\n\n        // total counts\n        getCounterOfTotalCounts(request, handler).ifPresent(counter -> counter.increment());\n        // different response code count\n        getCounterOfRespCodeCounts(request, response, handler).ifPresent(counter -> counter.increment());\n        if (ex != null) {\n            // exception counts\n            getCounterOfExceptionCounts(request, handler).ifPresent(counter -> counter.increment());\n        }\n    }\n\n}\n```\n\n代码有点长，解释一下：\n\n1. 自动注入MeterRegistry，老套路了\n\n2. getCounterOfXXX几个方法，通过request、handler来生成具体的监控项名称和标签，形如：app_requests_method_count.method.UserController.getUser。\n\n3. preHandle中预设了ThreadLocal的定时器\n\n4. recordTimeDistribution使用了Distribution，这是一个可以统计百分位的MicroMeter组件，类似Prometheus的histogram功能的你应该能秒懂。\n\n5. afterCompletion根据前面定时器，计算本次请求时间，并记录到Distributon中。\n\n6. afterCompletion记录总请求数、分resp.code的请求数、出错请求数。\n\n我们打开jconsole看下：\n\n![f](./jconsole2.png)\n\n在之前meters的基础上，新增了histogram分类，里面会详细记录请求时间，比如我这里做了一些本地压测后，.99时间是12ms，.95时间是1ms。\n\n在上面的基础上稍做修改，就可以投入使用了。\n\n感兴趣的话，你可以探索如何对Dubbo、gRPC等RPC接口进行应用程序监控项。\n\n本篇文章的代码，我放到了[homs-micrometer这个github项目](https://github.com/liheyuan/homs-micrometer)中，感兴趣的话可以查阅。\n"
  },
  {
    "path": "src/ch04-ms-dev3/skywalking.md",
    "content": "# 基于SkyWalking的链路追踪系统\n\n链路追踪提供了分布式调用链路的还原、统计、分析等功能，是提升微服务诊断效率的重要环节。\n\n本节，我们将基于[SkyWalking](https://skywalking.apache.org/)搭建链路追踪系统。\n\nSkyWalking是一款开源的APM(Application Performance Monitor)工具，以Java Agent + 插件化的方式运行。2019年其从孵化器毕业，正式成为Apache的顶级项目。\n\n## 单机实验\n\n我们首先跑通单机版的链路追踪。\n\nSkyWalking支持多种后台存储，这里我们选用ElasticSearch：\n\n```shell\n#!/bin/bash\n\nNAME=\"elasticsearch\"\nPUID=\"1000\"\nPGID=\"1000\"\n\nVOLUME=\"$HOME/docker_data/elasticsearch\"\nmkdir -p $VOLUME \n\ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    --env discovery.type=single-node \\\n    --volume \"$VOLUME:/usr/share/elasticsearch/data\" \\\n    -p 9200:9200 \\\n    -p 9300:9300 \\\n    --detach \\\n    --restart always \\\n    docker.elastic.co/elasticsearch/elasticsearch:7.15.2\n```\n\n接着，我们启动SkyWalking的后台服务：\n\n```shell\n#!/bin/bash\n\nNAME=\"skywalking\"\nPUID=\"1000\"\nPGID=\"1000\"\n\ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    -e SW_STORAGE=elasticsearch7 \\\n    -e SW_STORAGE_ES_CLUSTER_NODES=\"10.1.172.136:9200\" \\\n    -p 12800:12800 \\\n    -p 11800:11800 \\\n    --detach \\\n    --restart always \\\n    apache/skywalking-oap-server:8.7.0-es7\n```\n\n最后，启动SkyWalking的UI服务：\n\n```shell\n#!/bin/bash\n\nNAME=\"skywalkingui\"\nPUID=\"1000\"\nPGID=\"1000\"\n\ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    -e SW_OAP_ADDRESS=\"http://10.1.172.136:12800\" \\\n    -p 8080:8080 \\\n    --detach \\\n    --restart always \\\n    apache/skywalking-ui:8.7.0\n```\n\n上述，我们让容器直接使用了Host Net：10.1.172.136。\n\n下一步，我们下载最新版的[Java Agent](https://dlcdn.apache.org/skywalking/java-agent/8.8.0/apache-skywalking-java-agent-8.8.0.tgz)，其支持的框架可以在[这里]([Tracing and Tracing based Metrics Analyze Plugins | Apache SkyWalking](https://skywalking.apache.org/docs/skywalking-java/latest/en/setup/service-agent/java-agent/supported-list/))查看。\n\n解压后，我们直接使用java命令行运行：\n\n```shell\njava -javaagent:./skywalking-agent/skywalking-agent.jar -Dskywalking.agent.service_name=homs-start -Dskywalking.collector.backend_service=10.1.172.136:11800 -jar ./homs-start-0.0.1-SNAPSHOT.jar\n```\n\n如上所示：\n\n- 服务名字：homs-start\n\n- SkyWalking后台服务地址：10.1.172.136:11800\n\n启动成功后，我们尝试访问端口：\n\n```shell\ncurl \"127.0.0.1:8080\"\n```\n\n查看SkyWalking的UI，可以发现，已经统计到了链路追踪！\n\n![f](./skywalking-ui.png)\n\n## Kubernets中部署SkyWalking\n\n在Kubernets环境中，我们倾向只部署无状态服务，以便拓展。\n\n而对于SkyWaling Server这种服务，会占用较大性能，且没有太多需要扩展的场景，因此我们维持其外部部署方式，不上k8s。\n\n回顾下之前的内容，我们的homs-start是通过Docker镜像的方式启动的Pod和Deployment。\n\n我们需要对其进行改造，添加initContainer，注入Java Agent：\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: homs-start-deployment\n  labels:\n    app: homs-start\nspec:\n  selector:\n    matchLabels:\n      app: homs-start\n  replicas: 1\n  strategy:\n    type: RollingUpdate\n  template:\n    metadata:\n      labels:\n        app: homs-start\n    spec:\n      volumes:\n        - name: skywalking-agent\n          emptyDir: {}\n      containers:\n        - name: homs-start-server\n          image: coder4/homs-start:107\n          ports:\n            - containerPort: 8080\n          volumeMounts:\n            - name: skywalking-agent\n              mountPath: /skywalking\n          env:\n            - name: JAVA_TOOL_OPTIONS\n              value: -javaagent:/skywalking/agent/skywalking-agent.jar\n            - name: SW_AGENT_NAME\n              value: homs-start\n            - name: SW_AGENT_COLLECTOR_BACKEND_SERVICES\n              value: 10.1.172.136:11800\n          resources:\n            requests:\n              memory: 500Mi\n      initContainers:\n        - name: agent-container\n          image: apache/skywalking-java-agent:8.8.0-java8\n          volumeMounts:\n            - name: skywalking-agent\n              mountPath: /agent\n          command: [ \"/bin/sh\" ]\n          args: [ \"-c\", \"cp -R /skywalking/agent /agent/\" ]\n```\n\n如上所示：\n\n- 这里我们没有额外制作agent的镜像，而是使用了[官方的最新版](https://hub.docker.com/r/apache/skywalking-java-agent)\n\n- 我们添加了全局的临时Volume：skywalking-agent\n\n- 添加了initContainer：agent-container，主要负责启动时拷贝agent的jar包\n\n- 在启动homs-start-server时需要设定一些环境变量参数\n\n启动成功后，我们尝试登录minikube访问服务：\n\n```shell\nminikube ssh                                                                                         \nLast login: Tue Nov 16 07:54:28 2021 from 192.168.49.1\ndocker@minikube:~$ curl \"172.17.0.3:8080\"\n{\"timestamp\":\"2021-11-16T07:55:08.669+00:00\",\"status\":404,\"error\":\"Not Found\",\"path\":\"/\"}\n```\n\n然后查看SkyWalking的UI，也能成功记录到最新追踪请求！\n\n![f](./skywalking-k8s.png)\n\n至此，我们已经搭建了最基本的链路追踪系统，其还有很大的优化空间：\n\n- 官方agent镜像中包含了全量插件，你应当根据实际需要剪裁\n\n- 微服务中会有某些缺乏Agent插件的场景，需要自行定制插件\n\n- 不仅agent，服务的jar包其实也是可以通过initContainer来拷贝的，这可以进一步压缩镜像体积。\n\n上述优化，做为课后作业，留给喜欢挑战的你吧：-）\n"
  },
  {
    "path": "src/ch04-ms-dev3/victorialmetrics.md",
    "content": "# 基于VictoriaMetrics + Grafana的监控系统\n\n监控(Monitor)与度量(Metrics)是可观测性的重要环节。\n\n在本节中，我们将使用VirtorialMetrics构建自己的监控系统。\n\n提到监控系统的工具，你可能会想到老牌的Zabbix、Nagios，也可能听说过新星的Prometheus。\n\nPrometheus是一个开源的监控系统，凭借开放的生态环境、云原生等特性，逐步成为了微服务架构下的事实标准。\n\n然而，由于Prometheus设计初期并没有考虑存储扩展性，因此当监控的metrics升高到每秒百万级别后，会出现较为明显的性能瓶颈。\n\n[VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics)是进来快速崛起的开源监控项目，其在设计之处就支持水平拓展，并且兼容了Prometheus的协议，可以应对日益增长的metrics需求。\n\nGrafana是一款开源的可视化分析工具，通过丰富的仪表盘，让用户能够更直观的理解Metrics。\n\n本节，我们将基于Victoria-Metrics + Grafana搭建监控系统。\n\n## 安装VictoriaMetrics\n\n在下面的章节，我们将演示搭建vm的single版本，由于VM出色的性能，single已经足以应对中小企业的监控需求。你可以根据实际的需要，[部署集群版本]([HA monitoring setup in Kubernetes via VictoriaMetrics Cluster · VictoriaMetrics](https://docs.victoriametrics.com/guides/k8s-ha-monitoring-via-vm-cluster.html))。\n\n首先添加helm源\n\n```shell\nhelm repo add vm https://victoriametrics.github.io/helm-charts/\nhelm repo update\n```\n\n```shell\nhelm search repo vm/\n\n\nNAME                             CHART VERSION    APP VERSION    DESCRIPTION                                       \nvm/victoria-metrics-agent        0.7.34           v1.69.0        Victoria Metrics Agent - collects metrics from ...\nvm/victoria-metrics-alert        0.4.14           v1.69.0        Victoria Metrics Alert - executes a list of giv...\nvm/victoria-metrics-auth         0.2.33           1.69.0         Victoria Metrics Auth - is a simple auth proxy ...\nvm/victoria-metrics-cluster      0.9.12           1.69.0         Victoria Metrics Cluster version - high-perform...\nvm/victoria-metrics-k8s-stack    0.5.9            1.69.0         Kubernetes monitoring on VictoriaMetrics stack....\nvm/victoria-metrics-operator     0.4.2            0.20.3         Victoria Metrics Operator                         \nvm/victoria-metrics-single       0.8.12           1.69.0         Victoria Metrics Single version - high-performa...\n```\n\n我们查看所有可配置的参数选项：\n\n```shell\nhelm show values vm/victoria-metrics-single > values.yaml\n```\n\n将其修改为如下设置：\n\n```yaml\nserver:\n  persistentVolume:\n    enabled: false \n    accessModes:\n      - ReadWriteOnce\n    annotations: {}\n    storageClass: \"\"\n    existingClaim: \"\"\n    matchLabels: {}\n    mountPath: /storage\n    subPath: \"\"\n    size: 16Gi\n\n  scrape:\n    enabled: true\n    configMap: \"\"\n    config:\n      global:\n        scrape_interval: 15s\n      scrape_configs:\n        - job_name: victoriametrics\n          static_configs:\n            - targets: [ \"localhost:8428\" ]\n        - job_name: \"kubernetes-apiservers\"\n          kubernetes_sd_configs:\n            - role: endpoints\n          scheme: https\n          tls_config:\n            ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt\n            insecure_skip_verify: true\n          bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token\n          relabel_configs:\n            - source_labels:\n                [\n                    __meta_kubernetes_namespace,\n                    __meta_kubernetes_service_name,\n                    __meta_kubernetes_endpoint_port_name,\n                ]\n              action: keep\n              regex: default;kubernetes;https\n        - job_name: \"kubernetes-nodes\"\n          scheme: https\n          tls_config:\n            ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt\n            insecure_skip_verify: true\n          bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token\n          kubernetes_sd_configs:\n            - role: node\n          relabel_configs:\n            - action: labelmap\n              regex: __meta_kubernetes_node_label_(.+)\n            - target_label: __address__\n              replacement: kubernetes.default.svc:443\n            - source_labels: [ __meta_kubernetes_node_name ]\n              regex: (.+)\n              target_label: __metrics_path__\n              replacement: /api/v1/nodes/$1/proxy/metrics\n        - job_name: \"kubernetes-nodes-cadvisor\"\n          scheme: https\n          tls_config:\n            ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt\n            insecure_skip_verify: true\n          bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token\n          kubernetes_sd_configs:\n            - role: node\n          relabel_configs:\n            - action: labelmap\n              regex: __meta_kubernetes_node_label_(.+)\n            - target_label: __address__\n              replacement: kubernetes.default.svc:443\n            - source_labels: [ __meta_kubernetes_node_name ]\n              regex: (.+)\n              target_label: __metrics_path__\n              replacement: /api/v1/nodes/$1/proxy/metrics/cadvisor\n          metric_relabel_configs:\n            - action: replace\n              source_labels: [pod]\n              regex: '(.+)'\n              target_label: pod_name\n              replacement: '${1}'\n            - action: replace\n              source_labels: [container]\n              regex: '(.+)'\n              target_label: container_name\n              replacement: '${1}'\n            - action: replace\n              target_label: name\n              replacement: k8s_stub\n            - action: replace\n              source_labels: [id]\n              regex: '^/system\\.slice/(.+)\\.service$'\n              target_label: systemd_service_name\n              replacement: '${1}'\n```\n\n如上所述：\n\n- 我们禁用了PV，这将默认使用local的emptydir。建议你在生产环境，根据需要自行配置可自动装配的存储插件。\n\n- 从Kubernetes集群抓取信息，并做了一些label上的转化。\n\n- 如果你熟悉Prometheus的话，会发现上述配置和Prometheus基本是兼容的。\n\n安装vmsingle：\n\n```shell\nhelm install vmsingle vm/victoria-metrics-single -f ./values.yaml -n vm\nW1117 14:46:54.020279   26203 warnings.go:70] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+\nW1117 14:46:54.066766   26203 warnings.go:70] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+\nNAME: vmsingle\nLAST DEPLOYED: Wed Nov 17 14:46:53 2021\nNAMESPACE: vm\nSTATUS: deployed\nREVISION: 1\nTEST SUITE: None\nNOTES:\nThe VictoriaMetrics write api can be accessed via port 8428 on the following DNS name from within your cluster:\n    vmsingle-victoria-metrics-single-server.vm.svc.cluster.local\n\n\nMetrics Ingestion:\n  Get the Victoria Metrics service URL by running these commands in the same shell:\n    export POD_NAME=$(kubectl get pods --namespace vm -l \"app=server\" -o jsonpath=\"{.items[0].metadata.name}\")\n    kubectl --namespace vm port-forward $POD_NAME 8428\n\n  Write url inside the kubernetes cluster:\n    http://vmsingle-victoria-metrics-single-server.vm.svc.cluster.local:8428/api/v1/write\n\nRead Data:\n  The following url can be used as the datasource url in Grafana::\n    http://vmsingle-victoria-metrics-single-server.vm.svc.cluster.local:8428\n```\n\n上述的Read Data地址，后续需要用的，请复制、保存好。\n部署成功后，我们查看下Pod，运行成功：\n\n```shell\nkubectl get pods\ndefault       vmsingle-victoria-metrics-single-server-0   1/1     Running            0          59s\n```\n\n## 安装Grafana\n\n首先，依然是添加helm源：\n\n```shell\nhelm repo add grafana https://grafana.github.io/helm-charts\nhelm repo update\n```\n\n接下来，自定义参数并安装：\n\n```shell\ncat <<EOF | helm install my-grafana grafana/grafana -f -\n  datasources:\n    datasources.yaml:\n      apiVersion: 1\n      datasources:\n        - name: victoriametrics\n          type: prometheus\n          orgId: 1\n          url: http://vmsingle-victoria-metrics-single-server.default.svc.cluster.local:8428\n          access: proxy\n          isDefault: true\n          updateIntervalSeconds: 10\n          editable: true\n\n  dashboardProviders:\n   dashboardproviders.yaml:\n     apiVersion: 1\n     providers:\n     - name: 'default'\n       orgId: 1\n       folder: ''\n       type: file\n       disableDeletion: true\n       editable: true\n       options:\n         path: /var/lib/grafana/dashboards/default\n\n  dashboards:\n    default:\n      victoriametrics:\n        gnetId: 10229\n        revision: 21\n        datasource: victoriametrics\n      kubernetes:\n        gnetId: 14205\n        revision: 1\n        datasource: victoriametrics\nEOF\n```\n\n在上述配置中，我们添加了默认的数据源，使用前面创建好的VM地址。\n\n接着，我们获取Grafana的密码：\n\n```shell\nkubectl get secret --namespace default my-grafana -o jsonpath=\"{.data.admin-password}\" | base64 --decode ; echo\n\nSOnFX4CdrlyG5JACyBedk9mJk7btMz8cXjk7ZiOZ\n```\n\n然后代理端口到本地\n\n```shell\nexport POD_NAME=$(kubectl get pods --namespace default -l \"app.kubernetes.io/name=grafana,app.kubernetes.io/instance=my-grafana\" -o jsonpath=\"{.items[0].metadata.name}\")\n```\n\n```shell\nkubectl --namespace default port-forward $POD_NAME 3000\n```\n\n访问http://127.0.0.1:3000，使用admin / 前面的密码。\n\n如果一切顺利，会发现已经有Kubernetes集群的数据了：\n\n![f](./grafana.png)\n\n至此，我们搭建了基础的监控系统，你还可以做的更好：\n\n- 添加PV，让数据可以真正持久化\n\n- 部署分布式的版本\n\n- 在微服务中，暴露一些自定义监控指标，并将其抓取到VM的存储中\n"
  },
  {
    "path": "src/ch05-k8s/README.md",
    "content": "# 容器与编排系统\n\n最热门的容器方案 - Docker - 诞生于2013年。借助Namespace、cgroup、rootfs三大核心技术，Docker给软件开发、运维都来了颠覆性的体验。\n\n随着容器技术的普及，容器依赖、跨主机通信的需求日益显著。Kubernetes很好的解决了跨主机通信的问题，将容器管理集群化，容器编排系统化，并成为了容器编排系统的事实标准。\n\n本章将围绕Kubernetes展开，包括：\n\n1. 探索容器技术的意义\n\n2. 通过案例快速入门Kubernetees\n\n3. 基于Keepalived，搭建高可用的Kubernetes集群\n\n4. 介绍ingress等集群对外暴露的方式\n"
  },
  {
    "path": "src/ch05-k8s/container.md",
    "content": "# 从集装箱到容器\n\n容器化是一种全新的交付方式，它把应用及运行环境，整体打包成一个的镜像，从而保证了运行环境的统一。\n\n容器也是一种轻量级的隔离技术，在保证文件系统、网络、CPU等基础隔离的基础上，拥有更快的启动速度，更小的资源开销。\n\n还记得我们在第一章指出的微服务缺点么？运维难度、上线速度。这些都可以通过容器 / 容器编排技术得到管理。可以说，微服务架构的落地，离不开容器技术。\n\n## 从集装箱革命到运维革命\n\n这是著名的容器开源项目Docker的logo：\n\n![以](./docker_logo.png)\n\n你有没有注意到蓝色鲸鱼背上的东西呢？\n\n没错，是集装箱。\n\n尽管货物的海运已经出现了几百年，但直到20世纪中叶，货物运输依然是一种劳动力密集型工作。码头雇佣了数以万计的工人，将货物从岸上搬运到船舱中。由于货物的种类繁多，体积不一、传送带、铲车都不能根本的解决问题，货物装卸依然大量依赖人工，而且装卸时间大量占用了港口时间，装卸价格居高不下。\n\n20世纪50年代开始，集装箱逐渐走向商用的舞台。货物在岸上按照整齐的规格码放整齐，从而可以封装进集装箱。而装卸货物只需要机械来搬运集装箱即可，极大提高了港口的装卸效率。\n\n集装箱革命使得货物的装卸成本从5美元/吨骤降到16美分/吨，节约了97%......\n\n而以docker为代表的容器化项目的崛起，也推进了运维圈的另一场“革命”：容器就是集装箱，货物则是运行于容器中的，各式应用。\n\n容器技术的出现，根本性的解决了如下的技术难题：\n\n- 运行环境的标准化：为了让应用程序在生产机上跑起来，经常安装不同操作系统版本，不同版本的依赖软件库、环境配置......这些过程非常繁琐，还经常会由于版本的细微差异，和开发环境不一致，出现“这个程序本地好好的，放到服务器上就崩溃”这类情况。容器可以使用统一的描述语言(如Dockerfile)，快速构建出完全相同的、标准化环境，从而解决运行环境的问题。\n\n- 隔离化：如果都部署在一台物理机上，很可能会发生包、依赖冲突，导致无法运维，而容器可以为运行在同一台物理机上的应用程序，创建不同的隔离环境(文件系统、网络、物理资源等)。\n\ndocker是最成功的容器化项目之一，但并不是唯一的选项，这里列举几个强有力的竞争者：\n\n- rkt container：由RedHat主导的容器化项目，改进了Docker隔离模型的许多缺陷，具有更好的安全性能。\n\n- kata container：轻量级的定制化虚拟机(VM)，具有比肩容器化的技术，以及VM级别的隔离性。\n\n## 从容器到容器编排\n\n容器解决了运行环境标准化、隔离化的问题，但随着容器数量的不断提升增长，如何管理他们成为了一个新问题。\n\n容器编排指的是：自动化容器的部署、管理、扩展和联网。\n\n除了规模化，容器编排还面临以下问题：\n\n它解决了以下问题：\n\n- 应用不是孤立的，容器的运行也会有依赖关系，如何进行管理？\n\n- 如何在不影响业务运行的前提下，升级容器（内的应用）？\n\n- 如何在容器应用挂掉的时候，自动恢复故障？\n\n- 如何管理容器的本地存储？\n\n如今，Kubernetes已经成为容器编排领域的事实标准，它的特性有：\n\n- 滚动升级、回滚：支持渐进式的升级应用镜像版本，并可轻松地回滚到之前的版本。\n\n- 服务发现、负载均衡：内置了基于DNS / VIP的负载均衡机制。\n\n- 异构存储：CSI机制，支持云存储、NFS、Ceph等多种方式。\n\n- 密钥、配置存储：密码、配置等信息的存储。\n\n- 灵活的资源分配：支持多种资源保证方式，最大限度利用资源。\n\n- 批量任务：支持后台离线、批量任务。\n\n- 水平自动扩展：快速扩所容能力，支持人工 / 负载自适应\n\n- 健康、恢复：内置多种健康检查、自恢复机制\n\n- 拓展性：提供了多个扩展点，在不改变k8s代码的前提下，轻松拓展功能\n\n纸上得来终觉浅，简单的介绍是远不够的。\n\n在下一节，我们将使用一个例子，快速入门Kubernetes。\n"
  },
  {
    "path": "src/ch05-k8s/k8s-101.md",
    "content": "# 快速入门Kubernetes\n\n## 安装篇\n\n我们以Ubuntu为例，介绍Kubernetes基础工具的安装，若你使用其他操作系统，可以参考[官方文档](https://kubernetes.io/docs/tasks/tools/)。\n\n首先安装kubectl：\n\n```shell\nsudo apt-get update\nsudo apt-get install -y apt-transport-https ca-certificates curl\nsudo curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg\necho \"deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main\" | sudo tee /etc/apt/sources.list.d/kubernetes.list\nsudo apt-get update\nsudo apt-get install -y kubectl\n```\n\n接着，我们安装minikube，这是一个用于本地学习和测试的kubernetes集群：\n\n```shell\ncurl -LO https://storage.googleapis.com/minikube/releases/latest/minikube_latest_amd64.deb\nsudo dpkg -i minikube_latest_amd64.deb\n```\n\n## 启动minikube\n\n第一次启动Minikube，需要下载虚拟机、对应镜像，时间回稍长一些。\n\n```bash\nminikube start\n```\n\n成功后，我们看下状态：\n\n```shell\nminikube\ntype: Control Plane\nhost: Running\nkubelet: Running\napiserver: Running\nkubeconfig: Configured\n```\n\n如果需要关机，可以暂停 / 恢复minikube集群\n\n```shell\nminikube pause\nminikube resume\n```\n\n如果想重置minikube集群，可以使用删除后重新启动\n\n```shell\nminikube delete\n```\n\n## 部署你的第一个服务\n\n我们在minikube上部署一台nginx\n\n```shell\nkubectl create deployment my-nginx --image=nginx:stable\n```\n\n稍等片刻后，我们看下，已经创建成功：\n\n```shell\nkubectl get pod                               \nNAME                        READY   STATUS    RESTARTS   AGE\nmy-nginx-7bc876dc4b-r5zqr   1/1     Running   0          22s\n```\n\n我们查看下pod的信息，特别是IP\n\n```shell\nkubectl describe pod my-nginx-7bc876dc4b-r5zqr | grep IP\nIP:           172.17.0.3\n```\n\n我们尝试访问一下，发现无法成功：\n\n```bash\ncurl \"http://172.17.0.3\"\n```\n\n这是因为，minikube的网络环境，与我们本机是相互隔离的，我们需要先登录到minikube内，然后再尝试：\n\n```bash\nminikube ssh\n\ncurl \"http://172.17.0.3\"\n  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed\n100   612  100   612    0     0   597k      0 --:--:-- --:--:-- --:--:--  597k\n```\n\n成功！\n\n下面，我们退出minikube集群环境，尝试对nginx部署扩容：\n\n```bash\nkubectl scale deployment my-nginx --replicas=5\ndeployment.apps/my-nginx scaled\n```\n\n稍等片刻后，我们查看，发现扩容成功：\n\n```bash\nkubectl get pod                               \nNAME                        READY   STATUS    RESTARTS   AGE\nmy-nginx-7bc876dc4b-226g9   1/1     Running   0          60s\nmy-nginx-7bc876dc4b-872v2   1/1     Running   0          60s\nmy-nginx-7bc876dc4b-fvnwf   1/1     Running   0          60s\nmy-nginx-7bc876dc4b-fzr8s   1/1     Running   0          60s\nmy-nginx-7bc876dc4b-r5zqr   1/1     Running   1          5m36s\n```\n\n如何在mini集群外(例如我们本地)访问nginx呢？\n\n可以为上述deployment，暴露外部的LoadBalancer：\n\n```bash\nkubectl expose deployment my-nginx --type=LoadBalancer --port=80\n```\n\n我们看一下状态，会发现外部的IP是\"pending\"\n\n```shell\nkubectl get services\nNAME         TYPE           CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE\nkubernetes   ClusterIP      10.96.0.1     <none>        443/TCP        67m\nmy-nginx     LoadBalancer   10.104.5.62   <pending>     80:30229/TCP   37s  8m18s\n```\n\n需要启用minikube的隧道，来分配\"外部IP\"，这里的外部是相对于minikube而言的，实际上是我们本机网络的IP。\n\n```bash\nminikube tunnel\nkubectl get services                                            \nNAME         TYPE           CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE\nkubernetes   ClusterIP      10.96.0.1     <none>        443/TCP        67m\nmy-nginx     LoadBalancer   10.104.5.62   127.0.0.1     80:30229/TCP   24s\n 9m25s\n```\n\n启动隧道后，发现暴露到了127.0.0.1的80端口上，我们试一下：\n\n```bash\ncurl \"http://127.0.0.1:80\"  \n<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n    body {\n        width: 35em;\n        margin: 0 auto;\n        font-family: Tahoma, Verdana, Arial, sans-serif;\n    }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n```\n\nminikube也提供了可视化的Dashboard：\n\n```shell\nminikube dashboard --url\nhttp://127.0.0.1:59352/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/\n```\n\n在浏览器中打开上述连接，可以进入Web版的Dashboard，如下图所示：\n\n![f](./k8s-dashboard.png)\n\n至此，你已经通过在minikube上的实战演练，掌握了kubernetes的基本用法。\n\n在实际生产环境中，建议你搭建真实的分布式集群，不要使用minikube，我将在后续章节，介绍高可用k8s集群的部署。\n"
  },
  {
    "path": "src/ch05-k8s/k8s-cluster.md",
    "content": "# 搭建Kubernetes集群\n\n在本章的前几节，我们在minikube集群上，实战了很多内容，是时候搭建真正的集群了。\n\n本节，我们将借助kubeadm的帮助，搭建准生产级的k8s集群。\n\n关于\"准生产\"的含义，我们先放下不表。\n\n以下的集群搭建假设你使用Ubuntu的发行版，20.04，需要3台机器(可以是物理服务器，也可以是虚拟机，以下我们都简称机器)。\n\n如果你不是Ubuntu，请自行替换部分安装命令，很简单。\n\n## 1 调整系统参数\n\n我们需要调整一些系统参数，以方便后续集群的搭建。\n\n```shell\nlsmod | grep br_netfilter\nbr_netfilter\n\nsysctl net.bridge.bridge-nf-call-iptables\nnet.bridge.bridge-nf-call-iptables = 1\n\nsysctl net.bridge.bridge-nf-call-ip6tables\nnet.bridge.bridge-nf-call-ip6tables = 1\n\nswapoff -a\n```\n\n说明如下：\n\n- 需要开启netfilter\n\n- 调整对应内核参数如上\n\n- 关闭swap，建议你同步修改fstab(保证重启后生效)\n\n## 2 安装Docker\n\n首先安装Docker\n\n```shell\nsudo apt-get update && sudo apt-get install -y apt-transport-https\nsudo apt install -y docker.io\nsudo systemctl start docker\nsudo systemctl enable docker\n```\n\n接着，调整Docker默认组权限\n\n```shell\n# 将自己添加到docker组中\nsudo groupadd docker\nsudo gpasswd -a ${USER} docker\n# 重启后重新load下权限\nsudo service docker restart\nnewgrp - docker\n```\n\n最后，调整以下Docker的默认参数：\n\n```shell\nsudo vim /etc/docker/daemon.json\n\n{ \n  \"registry-mirrors\": [ \"https://registry.docker-cn.com\" ], \n  \"exec-opts\": [\"native.cgroupdriver=systemd\"] \n}\n```\n\n以上调整包含两部分：\n\n- 换成了docker的国内源，稳定但是速度并不快\n\n- 替换了cgroups驱动，这个主要是Ubuntu等几个发行版的问题，可以参考[这篇文章](https://www.coder4.com/archives/7344)\n\n以上操作完成后，我们重启Docker服务：\n\n```shell\nsudo service docker restart\n```\n\n## 3 安装Kubernetes相关二进制文件\n\n由于众所周知的原因，直接使用Google的apt仓库是不行的，我们直接用aliyun的(暂时没有focal的，这里沿用xenial的)。\n\n```shell\nsudo /etc/apt/source/xxx\ndeb http://mirrors.aliyun.com/kubernetes/apt kubernetes-xenial main\nsudo apt-get update\n```\n\n如果提示错误，自行import一下GPG key即可，请自行搜索。\n\n```shell\nsudo apt-get install -y kubelet kubeadm kubectl kubernetes-cni\n```\n\n最后启动\n\n```shell\nsudo systemctl status kubelet\n```\n\n如果是Run的状态是正常的，如果是Stopped，请查看日志，自行解决。\n\n### 4 安装Kubernetes所需要的镜像文件\n\nKubernets在启动时，会拉取大量了gcr.io上的容器镜像。\n\n由于众所周知的原因，这些国内是无法访问的。\n\n我们可以先将镜像离线下载到本地，再继续安装。\n\n先看一眼需要哪些镜像，这里需要设定版本，我们用当前最新版1.22.1：\n\n```shell\nkubeadm config images list --kubernetes-version v1.22.1\nk8s.gcr.io/kube-apiserver:v1.22.1\nk8s.gcr.io/kube-controller-manager:v1.22.1\nk8s.gcr.io/kube-scheduler:v1.22.1\nk8s.gcr.io/kube-proxy:v1.22.1\nk8s.gcr.io/pause:3.5\nk8s.gcr.io/etcd:3.5.0-0\nk8s.gcr.io/coredns/coredns:v1.8.4\n```\n\n这里我们使用阿里云的国内镜像，我这里使用awk的方式提供执行命令，你可以将输出结果直接黏贴到shell中执行。\n\n第一步，拉取镜像：\n\n```shell\nkubeadm config images list --kubernetes-version v1.22.1 | awk -F \"/\" '{print \"docker pull registry.aliyuncs.com/google_containers/\"$NF\"\"}'\n\n\ndocker pull registry.aliyuncs.com/google_containers/kube-apiserver:v1.22.1\ndocker pull registry.aliyuncs.com/google_containers/kube-controller-manager:v1.22.1\ndocker pull registry.aliyuncs.com/google_containers/kube-scheduler:v1.22.1\ndocker pull registry.aliyuncs.com/google_containers/kube-proxy:v1.22.1\ndocker pull registry.aliyuncs.com/google_containers/pause:3.5\ndocker pull registry.aliyuncs.com/google_containers/etcd:3.5.0-0\n# 最后这个要稍微特殊处理下\ndocker pull coredns/coredns:1.8.4\n```\n\n第二步，镜像tag重命名：（原因：我们换了镜像，一些前缀和tag会不对）：\n\n```shell\nkubeadm config images list --kubernetes-version v1.22.1 | awk -F \"/\" '{print \"docker tag registry.aliyuncs.com/google_containers/\"$2\" k8s.gcr.io/\"$NF\"\"}'\n\ndocker tag registry.aliyuncs.com/google_containers/kube-apiserver:v1.22.1 k8s.gcr.io/kube-apiserver:v1.22.1\ndocker tag registry.aliyuncs.com/google_containers/kube-controller-manager:v1.22.1 k8s.gcr.io/kube-controller-manager:v1.22.1\ndocker tag registry.aliyuncs.com/google_containers/kube-scheduler:v1.22.1 k8s.gcr.io/kube-scheduler:v1.22.1\ndocker tag registry.aliyuncs.com/google_containers/kube-proxy:v1.22.1 k8s.gcr.io/kube-proxy:v1.22.1\ndocker tag registry.aliyuncs.com/google_containers/pause:3.5 k8s.gcr.io/pause:3.5\ndocker tag registry.aliyuncs.com/google_containers/etcd:3.5.0-0 k8s.gcr.io/etcd:3.5.0-0\n# 特殊处理\ndocker tag coredns/coredns:1.8.4 k8s.gcr.io/coredns/coredns:v1.8.4\n```\n\n第三步，删除重命名之前的废弃tag：\n\n```shell\nkubeadm config images list --kubernetes-version v1.22.1 | awk -F \"/\" '{print \"docker rmi registry.aliyuncs.com/google_containers/\"$2\"\"}'\n\ndocker rmi registry.aliyuncs.com/google_containers/kube-apiserver:v1.22.1\ndocker rmi registry.aliyuncs.com/google_containers/kube-controller-manager:v1.22.1\ndocker rmi registry.aliyuncs.com/google_containers/kube-scheduler:v1.22.1\ndocker rmi registry.aliyuncs.com/google_containers/kube-proxy:v1.22.1\ndocker rmi registry.aliyuncs.com/google_containers/pause:3.5\ndocker rmi registry.aliyuncs.com/google_containers/etcd:3.5.0-0\n# 特殊处理\ndocker rmi coredns/coredns:1.8.4\n```\n\n最后，让我们确认下本地有哪些镜像：\n\n```shell\ndocker images\nREPOSITORY                           TAG       IMAGE ID       CREATED        SIZE\nk8s.gcr.io/kube-apiserver            v1.22.1   f30469a2491a   3 weeks ago    128MB\nk8s.gcr.io/kube-proxy                v1.22.1   36c4ebbc9d97   3 weeks ago    104MB\nk8s.gcr.io/kube-controller-manager   v1.22.1   6e002eb89a88   3 weeks ago    122MB\nk8s.gcr.io/kube-scheduler            v1.22.1   aca5ededae9c   3 weeks ago    52.7MB\nk8s.gcr.io/etcd                      3.5.0-0   004811815584   3 months ago   295MB\nk8s.gcr.io/coredns/coredns           v1.8.4    8d147537fb7d   3 months ago   47.6MB\nk8s.gcr.io/pause                     3.5       ed210e3e4a5b   6 months ago   683kB\n```\n\n## 5 初始化集群\n\n上述准备操作，需要在3台机器都执行。\n\n当准备妥当后，我们要初始化集群了，选择一台机器做为主节点(Master)，我们假设这台的地址是192.168.6.91：\n\n```shell\nsudo kubeadm init --kubernetes-version v1.22.1 --apiserver-advertise-address=192.168.6.91 --pod-network-cidr=10.6.0.0/16\n```\n\n上述的参数要解释下：\n\n- 集群版本1.22.1\n\n- api主控服务器的地址192.168.6.91\n\n- pod网络的地址是10.6.0.0/16，这里强制指定了，后面我们设定网络插件时会用。\n\n上述执行成功后，会有一个提示，类似如下，复制出来，后面要用到：\n\n```shell\n...\nkubeadm join 10.3.96.3:6443 --token w1zh7w.l6chof87e113m8u7 --discovery-token-ca-cert-hash sha256:5c010cce4123abcf6c48fd98f8559b33c1efc80742270d7493035a92adf53602\n...\n```\n\n我们初始化配置：\n\n```shell\nmkdir -p $HOME/.kube\nsudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config\nsudo chown $(id -u):$(id -g) $HOME/.kube/config\n```\n\n如果一切顺利，我们安装网络插件，这里以Weave为例：\n\n```shell\nkubectl apply -f \"https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d '\\n')\"\n```\n\n至此，主节点(Master)就配置完成了，我们继续配置其他节点。\n\n## 6 其他节点加入集群\n\n在其他节点上，执行前面记录的kubeadm join命令，都执行后，等一会，回到Master节点上，集群已经ready：\n\n```shell\nkubectl get nodes\nNAME STATUS ROLES AGE VERSION\nk8s1 Ready master 2m v1.14.3\nk8s2 Ready <none> 40s v1.14.3\nk8s3 Ready <none> 28s v1.14.3\n```\n\n## 7 测试和重置\n\n我们部署一个nginx的pod\n\n```shell\nkubectl run nginx --image=nginx\n```\n\n在某一台机器上测试：\n\n```shell\nkubectl describe pod nginx | grep ip\n10.6.0.194\ncurl \"10.6.0.194\"\n```\n\n成功！\n\n至此，我们完成了“准生产集群”的搭建，这里准生产的意思是：他已经具备了集群特性，但还不具备高可用的能力，我们会在下一节介绍Kubernetes集群的高可用。\n"
  },
  {
    "path": "src/ch05-k8s/k8s-ha-cluster.md",
    "content": "# 搭建Kubernetes高可用集群\n\n在上一节，我们介绍了Kubernetes集群的搭建，我们说这是一个“准生产”级别的集群。\n\n原因是，他不支持高可用。\n\n设想下，假设Master节点挂掉，会出现什么情况？\n\n由于只有一个主节点，所以集群会直接瘫痪。\n\n本节，我们将借助KeepAlived搭建一个高可用的集群。\n\n我们需要4台机器(物理机 or 虚拟机均可)。假设，这4台机器的IP分别为：\n\n- h1：192.168.1.12\n\n- h2：192.168.1.10\n\n- h3：192.168.1.9\n\n- h4：192.168.1.16\n\n同时我们需要一个不冲突的VIP(Virtual IP)，当发生主备切换时，KeepAlive会让VIP从主Master切换到备Master上。\n\n注意，如果你使用云主机，由于网络安全性的原因，是无法自由使用云主机的，需要单独HAVIP(高可用VIP)，申请地址如下：[腾讯云]([腾讯云运营活动 - 腾讯云](https://curl.qcloud.com/4D1bXeBP))，[阿里云]([阿里云登录 - 欢迎登录阿里云，安全稳定的云计算服务平台](https://page.aliyun.com/form/act367774547/index.htm?spm=a2c4g.11186623.0.0.1acd3d30qHqmc9))。\n\n这里假设你已经有了可用的VIP，其地址为192.168.1.8。\n\n## 1 部署KeepAlived\n\n这里我们选用h1、h2做为Master节点的主机和备机。\n\n则需要在这两台机器上安装keepalived\n\n```shell\nyum install -y keepalived\n```\n\n两台机器的配置文件分别如下：\n\nh1：\n\n```shell\n! Configuration File for keepalived\nglobal_defs {\n    router_id LVS_DEVEL \n}\nvrrp_script check_apiserver {\n    script \"</dev/tcp/127.0.0.1/6443\"\n    interval 1\n    weight -2\n}\nvrrp_instance VI-kube-master {\n    state MASTER     # 定义节点角色\n    interface eth0   # 网卡名称\n    virtual_router_id 68\n    priority 100\n    dont_track_primary\n    advert_int 3\n    authentication {\n        auth_type PASS\n        auth_pass mypass\n    }\n    unicast_src_ip 192.168.1.12  #当前ECS的ip\n    unicast_peer {\n        192.168.1.10             #对端ECS的ip\n    }\n    virtual_ipaddress {\n         192.168.1.8 # havip\n   }\n   track_script {\n         check_apiserver\n   }\n}\n```\n\nh2：\n\n```shell\n! Configuration File for keepalived\nglobal_defs {\n    router_id LVS_DEVEL \n}\nvrrp_script check_apiserver {\n    script \"</dev/tcp/127.0.0.1/6443\"\n    interval 1\n    weight -2\n}\nvrrp_instance VI-kube-master {\n    state BACKUP     # 定义节点角色\n    interface eth0   # 网卡名称\n    virtual_router_id 68\n    priority 99\n    dont_track_primary\n    advert_int 3\n    unicast_src_ip 192.168.1.10  #当前ECS的ip\n    authentication {\n        auth_type PASS\n        auth_pass mypass\n    }\n    unicast_peer {\n        192.168.1.12             #对端ECS的ip\n    }\n    virtual_ipaddress {\n         192.168.1.8 # havip\n    }\n    track_script {\n         check_apiserver\n    }\n}\n```\n\n解释如下：\n\n- h1做为主机，state是MASTER，h2备机，状态为BACKUP\n\n- h1和h2通过unicast方式发现，互相设置了unicast_peer为对方的IP\n\n- virtual_ipaddress中设置了相同的VIP地址\n\n- 检查是否可用使用了check_apiserver这个方法，他会检查TCP端口的6443是否开启。这实际是Kubernetes的API Server地址。\n\n配置完成后，记得重启两台机器的keepalived服务。\n\n```shell\nsystemctl enable keepalived\nservice keepalived start\n```\n\n## 2 准备Kubernetes环境\n\n这里与上一节的准备工作完全一致，不再赘述。\n\n请参考[《搭建Kubernetes集群》](./k8s-cluster.md)一节中的步骤2～4。\n\n注意这里是4台机器都要安装。\n\n## 3 启动主节点\n\n我们首先在h1上操作，命令如下：\n\n```shell\nkubeadm init --kubernetes-version v1.22.1 --control-plane-endpoint=192.168.1.8:6443  --apiserver-advertise-address=192.168.1.8 --pod-network-cidr=10.6.0.0/16 --upload-certs\n```\n\n说明如下：\n\n- 这里的control-plane-endpoint / apiserver-advertise-address填写的是VIP地址，会被VIP转发流量到h1 or h2上(取决于谁的状态是MASTER)\n\n- upload-certs：自动上传证书，高可用集群需要\n\n执行成功后，结果如下：\n\n```shell\nYour Kubernetes control-plane has initialized successfully!\n\nTo start using your cluster, you need to run the following as a regular user:\n\n  mkdir -p $HOME/.kube\n  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config\n  sudo chown $(id -u):$(id -g) $HOME/.kube/config\n\nAlternatively, if you are the root user, you can run:\n\n  export KUBECONFIG=/etc/kubernetes/admin.conf\n\nYou should now deploy a pod network to the cluster.\nRun \"kubectl apply -f [podnetwork].yaml\" with one of the options listed at:\n  https://kubernetes.io/docs/concepts/cluster-administration/addons/\n\nYou can now join any number of the control-plane node running the following command on each as root:\n\n  kubeadm join 192.168.1.8:6443 --token ydkjeh.zu9qthjssivlyrqy \\\n  --discovery-token-ca-cert-hash sha256:87d31b2fb17002f23dce01054c4877b133c15e3a1ed639e8f63b247f61609f8d \\\n  --control-plane --certificate-key 23474fd4262f1bf8849c5cea160fd3309621f79460266c43dfca1d7cc390f1af\n\nPlease note that the certificate-key gives access to cluster sensitive data, keep it secret!\nAs a safeguard, uploaded-certs will be deleted in two hours; If necessary, you can use\n\"kubeadm init phase upload-certs --upload-certs\" to reload certs afterward.\n\nThen you can join any number of worker nodes by running the following on each as root:\n\nkubeadm join 192.168.1.8:6443 --token ydkjeh.zu9qthjssivlyrqy \\\n  --discovery-token-ca-cert-hash sha256:87d31b2fb17002f23dce01054c4877b133c15e3a1ed639e8f63b247f61609f8d\n```\n\n上述有两个join命令，长的那个是master用的，短的是slave用的。\n\n我们将h2和h3也以master方式加入(因为Kubernetes要求至少有两个Master存活，才能正常工作)，也即在h2和h3上执行：\n\n```shell\nkubeadm join 192.168.1.8:6443 --token ydkjeh.zu9qthjssivlyrqy \\\n  --discovery-token-ca-cert-hash sha256:87d31b2fb17002f23dce01054c4877b133c15e3a1ed639e8f63b247f61609f8d \\\n  --control-plane --certificate-key 23474fd4262f1bf8849c5cea160fd3309621f79460266c43dfca1d7cc390f1af\n```\n\n## 4 启动普通节点\n\n在h4上以slave身份加入\n\n```shell\nkubeadm join 192.168.1.8:6443 --token ydkjeh.zu9qthjssivlyrqy \\\n  --discovery-token-ca-cert-hash sha256:87d31b2fb17002f23dce01054c4877b133c15e3a1ed639e8f63b247f61609f8d\n```\n\n## 5 安装网络插件\n\n回到h1 or h2 or h3上执行(因为他们三个都是Master)：\n\n```shell\nwget https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml\n# 修改cidr匹配后\nkubectl apply -f ./kube-flannel.yml\n```\n\n## 6 测试高可用\n\n我们对h1执行关机\n\n```shell\npoweroff\n```\n\n然后查看h2上的keepalived日志，可以观察到切换：\n\n```shell\n9月 18 7:59:28 h2 Keepalived_vrrp[18653]: VRRP_Instance(VI-kube-master) Changing effective priority from 97 to 99\n9月 18 8:03:22 h2 Keepalived_vrrp[18653]: VRRP_Instance(VI-kube-master) Transition to MASTER STATE\n9月 18 8:03:25 h2 Keepalived_vrrp[18653]: VRRP_Instance(VI-kube-master) Entering MASTER STATE\n9月 18 8:03:25 h2 Keepalived_vrrp[18653]: VRRP_Instance(VI-kube-master) setting protocol VIPs.\n```\n\n然后立即在h2上查看集群状态，全部正常：\n\n```shell\nkubectl get nodes\nNAME   STATUS     ROLES                  AGE     VERSION\nh1     Ready   control-plane,master   6m16s   v1.22.2\nh2     Ready      control-plane,master   5m51s   v1.22.2\nh3     Ready      control-plane,master   4m52s   v1.22.2\nh4     Ready      <none>                 3m38s   v1.22.2\n```\n\n再等一会后，发现h1挂掉了：\n\n```shell\nkubectl get nodes\nNAME   STATUS     ROLES                  AGE     VERSION\nh1     NotReady   control-plane,master   6m16s   v1.22.2\nh2     Ready      control-plane,master   5m51s   v1.22.2\nh3     Ready      control-plane,master   4m52s   v1.22.2\nh4     Ready      <none>                 3m38s   v1.22.2\n```\n\n至此，我们实现了Master的高可用！\n\n## 7 测试高可用恢复\n\n我们重启启动h1，稍等一会，发现一切正常！\n\n```shell\nkubectl get nodes\nNAME   STATUS   ROLES                  AGE     VERSION\nh1     Ready    control-plane,master   8m14s   v1.22.2\nh2     Ready    control-plane,master   7m49s   v1.22.2\nh3     Ready    control-plane,master   6m50s   v1.22.2\nh4     Ready    <none>                 5m36s   v1.22.2\n```\n\n至此，你应该已经熟悉了Kubernetes集群高可用的搭建步骤。\n\n这里提一个问题：我们将h1、h2、h3都是Master，但是只在h1和h2上设置了KeepAlived。\n\n- 如果h3挂掉后，集群能正常工作么？\n\n- 如果h3挂掉后，h2也挂掉了，集群还能正常工作么？\n"
  },
  {
    "path": "src/ch05-k8s/k8s-ingress.md",
    "content": "# 通过ingress暴露内部服务\n\n在kubernetes集群中，有一个常见的需求：如何将内部服务暴露出来，供外部访问？\n\n在[快速入门Kubernetes](k8s-101.md)一节中，我们使用了Service(Load Balancer)的方式，对外暴露了nginx服务。试想：如果我们有100个内部Deployment，能够使用LB的方式，对外暴露么？\n\n如果你还有印象，LB的对外暴露，要占用一个独立的端口，当需要暴露的服务增多时，光是端口的占用和分配，就已经是一个头疼的问题了。\n\n实际上，Kubernetes为我们提供了三种暴露内部服务的机制：\n\n- NodePort：在Kubernetes的所有节点上，开放一个端口，转发到内部具体的service上，与LoadBalancer相比，它不会绑定外网IP，多用于临时用途(如debug)\n\n- LoadBalancer：每个服务可以绑定一个外网IP、端口，当需要暴露的服务不多时，这是官方推荐的选择。\n\n- Ingress：像一个“智能路由器”，对外只暴露一个IP/端口，可以根据路径、头信息等变量，自动转发到内部的多个不同服务上。\n\n本节，我们将介绍两种不同的Ingress，来实现“暴露内部多组服务这个需求”。\n\n## 七层ingress\n\n首先，我们来看一下Nginx Ingress Controller，这是一款较早退出的Ingress方案，基于Nginx实现了应用层(http)协议的暴露。\n\n我们在上一节的基础上，添加另一组deployment：\n\n```bash\nkubectl create deployment my-httpd --image=httpd:2.4\nkubectl scale deployment my-httpd --replicas=3\n```\n\n同时，我们将之前创建的ngxin，也缩容为3：\n\n```bash\nkubectl scale deployment my-nginx --replicas=3\nkubectl get pods                              \nNAME                        READY   STATUS    RESTARTS   AGE\nmy-httpd-84bdf5b4d9-jjvwv   1/1     Running   0          46s\nmy-httpd-84bdf5b4d9-n269p   1/1     Running   0          16s\nmy-httpd-84bdf5b4d9-rw2kk   1/1     Running   0          16s\nmy-nginx-7bc876dc4b-226g9   1/1     Running   2          4h46m\nmy-nginx-7bc876dc4b-872v2   1/1     Running   2          4h46m\nmy-nginx-7bc876dc4b-fzr8s   1/1     Running   2          4h46m\n```\n\n接着，我们创建上述两个deployment的service：\n\n```bash\nkubectl expose deployment/my-nginx --port=80\nkubectl expose deployment/my-httpd --port=80\n\nkubectl get services\nNAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE\nkubernetes   ClusterIP   10.96.0.1       <none>        443/TCP   9m16s\nmy-httpd     ClusterIP   10.102.22.9     <none>        80/TCP    4s\nmy-nginx     ClusterIP   10.109.10.111   <none>        80/TCP    9s\n```\n\n在配置ingress之前，我们首先要启用ingress：\n\n```bash\nminikube addons enable ingress\n```\n\n如果你使用的是MacOS，可能会报错，此时需要一些额外的配置，请参考这个[帖子](https://github.com/kubernetes/minikube/issues/7332)。\n\n接下来，我们创建ingress.yaml文件：\n\n```yaml\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: homs-ingress\n  annotations:\n    kubernetes.io/ingress.class: nginx  \n    nginx.ingress.kubernetes.io/rewrite-target: /$1\nspec:\n  tls:\n  - hosts:\n    - homs.coder4.com\n    secretName: homs-secret\n  rules:\n  - host: homs.coder4.com\n    http:\n      paths:\n      - path: /my-nginx/?(.*)\n        pathType: Prefix\n        backend:\n          service:\n            name: my-nginx\n            port:\n              number: 80\n      - path: /my-httpd/?(.*)\n        pathType: Prefix\n        backend:\n          service:\n            name: my-httpd\n            port:\n              number: 80\n```\n\n解释一下：\n\n- 我们定义了Nginx的Ingress，并使用了转发前清除前缀（rewrite-target配置）\n\n- 定义了两个不同的前缀my-nginx和my-httpd，通过前缀指向内部服务\n\n- 同时支持了http和https解析，但https是自签证书，所以后面我们只用http\n\n然后创建它：\n\n```bash\nkubectl apply -f ./ingress.yaml\n```\n\n稍等一会，ingress的IP分配成功后如下所示：\n\n```bash\nkubectl get ingress\nNAME           CLASS    HOSTS             ADDRESS        PORTS     AGE\nhoms-ingress   <none>   homs.coder4.com   192.168.64.3   80, 443   34s\n```\n\n如上所示，“192.168.64.3”就是分配的ingressIP，但我们需要用DNS访问它，这里，我使用nip.io这个黑魔法来避免需要修改hosts的问题，即修改上述yaml中的host为“192.168.64.3.nip.io”。\n\n我们登录到minikube集群内部，尝试访问：\n\n```shell\ncurl -kL \"http://192.168.64.3.nip.io/my-httpd\"\n<html><body><h1>It works!</h1></body></html>\n\ncurl -kL \"http://192.168.64.3.nip.io/my-nginx\"\n<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\nhtml { color-scheme: light dark; }\nbody { width: 35em; margin: 0 auto;\nfont-family: Tahoma, Verdana, Arial, sans-serif; }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n```\n\n如上，我们成功的用prefix的路径(my-nginx / my-httpd)，访问了两个不同的内部service！\n\n## 修改转发前缀\n\n在上述的配置中，我们实现了多服务的转发，但准法后的location存在一些问题，我们换一个service验证一下：\n\n```shell\nkubectl create deployment service1 --image=mendhak/http-https-echo:23\nkubectl create deployment service2 --image=mendhak/http-https-echo:23\n```\n\n对外暴露服务：\n\n```shell\nkubectl expose deployment/service1 --port=8080\nkubectl expose deployment/service2 --port=808\n```\n\n修改一下ingress：\n\n```shell\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: homs-ingress\n  annotations:\n    kubernetes.io/ingress.class: nginx  \n    nginx.ingress.kubernetes.io/rewrite-target: /$1\nspec:\n  tls:\n  - hosts:\n    - homs.coder4.com\n    secretName: homs-secret\n  rules:\n  - host: homs.coder4.com\n    http:\n      paths:\n      - path: /service1/?(.*)\n        pathType: Prefix\n        backend:\n          service:\n            name: service1\n            port:\n              number: 8080\n      - path: /service2/?(.*)\n        pathType: Prefix\n        backend:\n          service:\n            name: service2\n            port:\n              number: 8080\n```\n\n登录到minikube后curl：\n\n```shell\n{\n  \"path\": \"/\",\n  \"headers\": {\n    \"host\": \"192.168.64.11.nip.io\",\n    \"x-request-id\": \"7a00b30a5d4fd4c084d2bcfbfd44f636\",\n    \"x-real-ip\": \"192.168.64.11\",\n    \"x-forwarded-for\": \"192.168.64.11\",\n    \"x-forwarded-host\": \"192.168.64.11.nip.io\",\n    \"x-forwarded-port\": \"443\",\n    \"x-forwarded-proto\": \"https\",\n    \"x-scheme\": \"https\",\n    \"user-agent\": \"curl/7.76.0\",\n    \"accept\": \"*/*\"\n  },\n  \"method\": \"GET\",\n  \"body\": \"\",\n  \"fresh\": false,\n  \"hostname\": \"192.168.64.11.nip.io\",\n  \"ip\": \"192.168.64.11\",\n  \"ips\": [\n    \"192.168.64.11\"\n  ],\n  \"protocol\": \"https\",\n  \"query\": {},\n  \"subdomains\": [\n    \"11\",\n    \"64\",\n    \"168\",\n    \"192\"\n  ],\n  \"xhr\": false,\n  \"os\": {\n    \"hostname\": \"service2-5686d4f68c-4vz7d\"\n  },\n  \"connection\": {}\n}\n```\n\n观察上述输出，发现转发后的location被重定向了，如果我们的服务想收到完整的请求，如何实现呢？\n\n我们可以修改ingress配置，在路径上添加一段分组匹配，如下：\n\n```yaml\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: homs-ingress\n  annotations:\n    kubernetes.io/ingress.class: nginx  \n    nginx.ingress.kubernetes.io/rewrite-target: /$1$2\n    nginx.ingress.kubernetes.io/app-root: /service1\nspec:\n  tls:\n  - hosts:\n    - homs.coder4.com\n    secretName: homs-secret\n  rules:\n  - host: 192.168.64.11.nip.io \n    http:\n      paths:\n      - path: /(service1/?)(.*)\n        pathType: Prefix\n        backend:\n          service:\n            name: service1\n            port:\n              number: 8080\n      - path: /(service2/?)(.*)\n        pathType: Prefix\n        backend:\n          service:\n            name: service2\n            port:\n              number: 8080\n\n```\n\n生效后，再次curl：\n\n```shell\ncurl -kL \"http://192.168.64.11.nip.io/service2\"\n{\n  \"path\": \"/service2\",\n  \"headers\": {\n    \"host\": \"192.168.64.11.nip.io\",\n    \"x-request-id\": \"b5759cf6f47d0ed713142178ddea4f96\",\n    \"x-real-ip\": \"192.168.64.11\",\n    \"x-forwarded-for\": \"192.168.64.11\",\n    \"x-forwarded-host\": \"192.168.64.11.nip.io\",\n    \"x-forwarded-port\": \"443\",\n    \"x-forwarded-proto\": \"https\",\n    \"x-scheme\": \"https\",\n    \"user-agent\": \"curl/7.76.0\",\n    \"accept\": \"*/*\"\n  },\n  \"method\": \"GET\",\n  \"body\": \"\",\n  \"fresh\": false,\n  \"hostname\": \"192.168.64.11.nip.io\",\n  \"ip\": \"192.168.64.11\",\n  \"ips\": [\n    \"192.168.64.11\"\n  ],\n  \"protocol\": \"https\",\n  \"query\": {},\n  \"subdomains\": [\n    \"11\",\n    \"64\",\n    \"168\",\n    \"192\"\n  ],\n  \"xhr\": false,\n  \"os\": {\n    \"hostname\": \"service2-5686d4f68c-4vz7d\"\n  },\n  \"connection\": {}\n}\n```\n\n成功！\n\nNginx Ingress也支持通过不同的Host来区分不同Service，也支持nginx的部分自定义配置，推荐你阅读[官方ingress例子]([Introduction - NGINX Ingress Controller](https://kubernetes.github.io/ingress-nginx/examples/))。\n\n## 四层ingress\n\n在上述两个例子中，我们实现了7层http协议的暴露 & 转发，ingress也支持4层的TCP协议。\n\n为了防止影响，我们首先重置集群，并重新启用ingress。\n\n```shell\nminikube delete\nminikube start\nminikube addons enable ingress\n```\n\n接着，创建一个TCP的服务，我们以redis为例：\n\n```shell\nkubectl create deployment redis --image=redis:6\n\nkubectl expose deployment/redis --port=6379\n```\n\n接着，我们创建映射关系，TCP的ingress是通过ConfigMap额外配置的。\n\n```yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: tcp-services\n  namespace: ingress-nginx\ndata:\n  6379: \"default/redis:6379\"\n```\n\n最后，我们将端口映射，修改到ingress上：\n\n```shell\nkubectl edit service -n ingress-nginx ingress-nginx-controller\n```\n\n在规则处添加如下代码：\n\n```yaml\n  - name: redis\n    port: 6379\n    protocol: TCP\n    targetPort: 6379\n```\n\n这里我们并没有填写nodePort，这是系统会自动分配的，不用我们手动处理。\n\n保存成功后，我们尝试通过ingress的端口连接：\n\n```shell\nkubectl get services --all-namespaces\nNAMESPACE       NAME                                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                                     AGE\ndefault         kubernetes                           ClusterIP   10.96.0.1       <none>        443/TCP                                     35m\ndefault         redis                                ClusterIP   10.109.20.237   <none>        6379/TCP                                    33m\ningress-nginx   ingress-nginx-controller             NodePort    10.110.48.51    <none>        80:30958/TCP,443:32737/TCP,6379:32765/TCP   34m\ningress-nginx   ingress-nginx-controller-admission   ClusterIP   10.103.12.249   <none>        443/TCP                                     34m\nkube-system     kube-dns                             ClusterIP   10.96.0.10      <none>        53/UDP,53/TCP,9153/TCP                      35m\n```\n\n我们本地使用redis连接：\n\n```shell\nredis-cli -h $(minikube ip) -p 32765\n> info\ninfo\n# Server\nredis_version:6.2.6\nredis_git_sha1:00000000\nredis_git_dirty:0\nredis_build_id:1527eab61b27d3bf\nredis_mode:standalone\nos:Linux 4.19.182 x86_64\narch_bits:64\nmultiplexing_api:epoll\n.....\n```\n\n成功！\n\n至此，我们成功使用Ingress暴露了内部的TCP端口。\n\n如果你仔细对比HTTP和TCP的Ingress，不难发现：\n\n- HTTP的Ingress更加实用，可以通过不同Host甚至不同Path，区分多个内部Service\n\n- TCP的Ingress相对来说，比较\"凑合\"，虽然能够工作，但配置繁琐、还需要耗费多个端口，并不方便。\n\n因此，再实际工作中，如果想从k8s集群外访问集群内的TCP服务，多采用网络打通的方式进行，我们将在后续章节介绍这一功能。\n"
  },
  {
    "path": "src/ch06-cd/README.md",
    "content": "# 持续交付流水线\n\n持续交付是敏捷开发的一种最佳实践，代码发生变更后，可以自动进行持续集成，测试，并部署到线上系统中。\n\n持续交付贯穿了软件的开发、测试、发布等全生命周期，也是微服务架构的基石。\n\n本节将借助Jenkins + 容器技术，打造自己的流水线，脉络是：\n\n1. Jenkins的部署、插件、基本用法\n\n2. Jenkins的Agent定制\n\n3. 基于Jenkins的交付流水线\n\n4. 交付流水线的改进\n\n经过本章的实战，你将获得一套生产级别的持续交付流水线。\n"
  },
  {
    "path": "src/ch06-cd/jenkins-custom.md",
    "content": "# Jenkins定制Agent\n\n上一节，我们实现了最简单的打包任务，在这一节，我们将定制所需的打包环境，为CD流水线做准备。\n\n## 手动连接Agent\n\n在上一节，我们使用了Kubernetes集群启动新的Slave节点，你可以沿着这条路，继续集成所需的环境，不再展开。\n\n在本节，我们将切换另一种思路，使用手动启动&连接的方式。\n\n首先，在Jenkins中添加一个Agent，路径是：Manage Jenkins -> Manage Nodes and Clouds -> New Node。\n\n关键参数如下：\n\nname：自选，这里e1\n\nNumber of executors：在这台机器上的并发执行任务数，这里选默认的2\n\nRemote root directory：默认执行目录，这里选/home/jenkins/ateng\n\nLabels：自选，这里executor，可以用它对Executor分组(如测试、线上等)\n\nLaunch method：Launch agent by connecting it to the master，即我们手动连接\n\n保存后，点击进去后，能看到如下提示：\n\n```shell\nRun from agent command line:\n\njava -jar agent.jar -jnlpUrl http://127.0.0.1:8080/computer/e1/jenkins-agent.jnlp -secret b057970bf978f53a8f945d470ac644e44c945e4b7259b216f703dedb95d0cac9 -workDir \"/home/jenkins/agent\"\nRun from agent command line, with the secret stored in a file:\n\necho b057970bf978f53a8f945d470ac644e44c945e4b7259b216f703dedb95d0cac9 > secret-file\njava -jar agent.jar -jnlpUrl http://127.0.0.1:8080/computer/e1/jenkins-agent.jnlp -secret @secret-file -workDir \"/home/jenkins/agent\"\n```\n\n如上所示，我们需要用上述的Secret来连接Controller(主控)节点。\n\n我们通过Docker启动Executor节点，如下：\n\n```shell\n#!/bin/bash\n\nNAME=\"jenkins_e1\"\nPUID=\"1000\"\nPGID=\"1000\"\n\ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --name $NAME \\\n    --env PUID=$PUID \\\n    --env PGID=$PGID \\\n    --detach \\\n    --init jenkins/inbound-agent \\\n    -workDir=/home/jenkins/agent \\\n    -url http://10.1.172.136:8080 \\\n    b057970bf978f53a8f945d470ac644e44c945e4b7259b216f703dedb95d0cac9 \\\n    e1\n```\n\n温馨提示：上述的workDir需要与Jenkins中的配置保持一致。\n\n当启动成功后，能看到节点上线了，如下图所示：\n\n![f](./jenkins-executor-online.png)\n\n为了不调度到Controller节点，我们可以将其上的执行数量设置为0。\n\n随后，我们尝试修改任务，如下所示：\n\n```groovy\npipeline {\n    agent any \n    stages {\n        stage('Test') { \n            steps {\n                sh 'echo hello world'\n            }\n        }\n    }\n}\n```\n\n如果一起顺利，其会成功地在e1完成执行！\n\n## 定制Executor的环境\n\n从上述例子中，不难理解：真正的打包任务，是在Executor中执行的。\n\n如果我们的打包流程需要用到git、Java、Gradle、Kubernetes的话，我们也需要将这些集成到Executor中。\n\n我们基于Jenkins的官方基础镜像进行定制，Dockerfile如下：\n\n```shell\nFROM jenkins/inbound-agent:latest-jdk8\n\nENV GRADLE_VERSION=7.2\nENV K8S_VERSION=v1.22.3\n\n# tool\nUSER root\nRUN apt-get update && \\\n    apt-get install -y curl unzip docker-ce docker-ce-cli && \\\n    apt-get clean\n\n# gradle\nRUN curl -skL -o /tmp/gradle-bin.zip https://services.gradle.org/distributions/gradle-$GRADLE_VERSION-bin.zip && \\\n    mkdir -p /opt/gradle && \\\n    unzip -q /tmp/gradle-bin.zip -d /opt/gradle && \\\n    ln -sf /opt/gradle/gradle-$GRADLE_VERSION/bin/gradle /usr/local/bin/gradle\n\nRUN chown -R 1001:0 /opt/gradle && \\\n    chmod -R g+rw /opt/gradle\n\n# kubectl\nRUN curl -LO https://storage.googleapis.com/kubernetes-release/release/$K8S_VERSION/bin/linux/amd64/kubectl\nRUN chmod +x ./kubectl\nRUN mv ./kubectl /usr/local/bin\n\nUSER jenkins\n```\n\n如上所示：\n\n- 我们基于inbound-agent进行定制，这是官方的默认的Agent基础镜像\n- 随后，我们使用apt安装curl、unzip等基础工具\n- 接着，我们安装gradle、kubectl等二进制文件\n- 最后恢复默认的运行环境\n\n制作镜像\n\n```shell\ndocker build -t \"coder4/jenkins-my-agent\" .\n```\n\n制作时间会比较长\n\n再次打包\n\n```groovy\npipeline {\n    agent {label 'executor'} \n    stages {\n        stage('git') {\n            steps {\n                sh \"echo todo\"\n            }\n        }\n\n        stage('gradle') {\n            steps {\n                sh \"gradle -v\"\n            }    \n        }\n\n        stage('k8s') { \n            steps {\n                withKubeConfig([credentialsId: \"60a8e9d2-0212-4ff4-aa98-f46fced97121\",serverUrl: \"https://kubernetes:6443\"]) {\n                    sh \"kubectl get nodes\"\n                }\n            }\n        }\n    }\n}\n```\n\n需要指出的是：上述'k8s'阶段，使用的凭据，是我们在[ Jenkins搭建入门](./jenkins-k8s.md)一节中生成的证书+凭据。\n\n运行结果\n\n```shell\nStarted by user admin\n[Pipeline] Start of Pipeline\n[Pipeline] node\nRunning on e1 in /home/jenkins/agent/workspace/test\n[Pipeline] {\n[Pipeline] stage\n[Pipeline] { (git)\n[Pipeline] sh\n+ git version\ngit version 2.30.2\n[Pipeline] }\n[Pipeline] // stage\n[Pipeline] stage\n[Pipeline] { (gradle)\n[Pipeline] sh\n+ gradle -v\n\nWelcome to Gradle 7.2!\n\nHere are the highlights of this release:\n - Toolchain support for Scala\n - More cache hits when Java source files have platform-specific line endings\n - More resilient remote HTTP build cache behavior\n\nFor more details see https://docs.gradle.org/7.2/release-notes.html\n\n\n------------------------------------------------------------\nGradle 7.2\n------------------------------------------------------------\n\nBuild time:   2021-08-17 09:59:03 UTC\nRevision:     a773786b58bb28710e3dc96c4d1a7063628952ad\n\nKotlin:       1.5.21\nGroovy:       3.0.8\nAnt:          Apache Ant(TM) version 1.10.9 compiled on September 27 2020\nJVM:          1.8.0_302 (Temurin 25.302-b08)\nOS:           Linux 5.10.47-linuxkit amd64\n\n[Pipeline] }\n[Pipeline] // stage\n[Pipeline] stage\n[Pipeline] { (k8s)\n[Pipeline] withKubeConfig\n[Pipeline] {\n[Pipeline] sh\n+ kubectl get nodes\nNAME       STATUS   ROLES                  AGE     VERSION\nminikube   Ready    control-plane,master   6h58m   v1.21.2\n[Pipeline] }\n[kubernetes-cli] kubectl configuration cleaned up\n[Pipeline] // withKubeConfig\n[Pipeline] }\n[Pipeline] // stage\n[Pipeline] }\n[Pipeline] // node\n[Pipeline] End of Pipeline\nFinished: SUCCESS\n```\n"
  },
  {
    "path": "src/ch06-cd/jenkins-k8s-optimize.md",
    "content": "# Jenkins优化Kubernetes部署流水线\n\n在上一节，我们实现了全链路的部署流水线。\n\n本节，我们将继续完善、优化部署水线。\n\n## Gradle加速\n\n首先，在之前的定制Agent中，我们使用了Gradle(Maven)的默认仓库。\n\n由于众所周知的原因，默认仓库的速度很慢、不稳定。\n\n这回严重降低打包流水线的速度，我们对这一问题进行优化。\n\n修改Agent的Dockerfile如下，增加Gradle仓库配置：\n\n```shell\nFROM jenkins/inbound-agent:latest-jdk8\n\nENV GRADLE_VERSION=7.2\nENV K8S_VERSION=v1.22.3\nENV DOCKER_CHANNEL stable\nENV DOCKER_VERSION 18.06.3-ce \n\n# tool\nUSER root\nRUN apt-get update && \\\n    apt-get install -y curl unzip sudo && \\\n    apt-get clean\n\n# docker\nRUN curl -fsSL \"https://download.docker.com/linux/static/${DOCKER_CHANNEL}/x86_64/docker-${DOCKER_VERSION}.tgz\" \\\n  | tar -xzC /usr/local/bin --strip=1 docker/docker\n\n# gradle\nRUN curl -skL -o /tmp/gradle-bin.zip https://services.gradle.org/distributions/gradle-$GRADLE_VERSION-bin.zip && \\\n    mkdir -p /opt/gradle && \\\n    unzip -q /tmp/gradle-bin.zip -d /opt/gradle && \\\n    ln -sf /opt/gradle/gradle-$GRADLE_VERSION/bin/gradle /usr/local/bin/gradle\n\nRUN chown -R 1001:0 /opt/gradle && \\\n    chmod -R g+rw /opt/gradle\n\n# kubectl\nRUN curl -LO https://storage.googleapis.com/kubernetes-release/release/$K8S_VERSION/bin/linux/amd64/kubectl\nRUN chmod +x ./kubectl\nRUN mv ./kubectl /usr/local/bin\n\n\n# add jenkins user to sudoer without password \nRUN usermod -aG sudo jenkins \nRUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers\n\n# jenkins\nUSER jenkins\n\n# gradle mirror\nENV GRADLE_CONFIG_DIR=/home/jenkins/.gradle \nRUN mkdir ${GRADLE_CONFIG_DIR}\nRUN echo \"Ly8gcHJvamVjdAphbGxwcm9qZWN0c3sKICAgIHJlcG9zaXRvcmllcyB7CgltYXZlbkxvY2FsKCkKICAgICAgICBtYXZlbiB7IHVybCAnaHR0cHM6Ly9tYXZlbi5hbGl5dW4uY29tL3JlcG9zaXRvcnkvcHVibGljLycgfQogICAgICAgIG1hdmVuIHsgdXJsICdodHRwczovL21hdmVuLmFsaXl1bi5jb20vcmVwb3NpdG9yeS9qY2VudGVyLycgfQogICAgICAgIG1hdmVuIHsgdXJsICdodHRwczovL21hdmVuLmFsaXl1bi5jb20vcmVwb3NpdG9yeS9nb29nbGUvJyB9CiAgICAgICAgbWF2ZW4geyB1cmwgJ2h0dHBzOi8vbWF2ZW4uYWxpeXVuLmNvbS9yZXBvc2l0b3J5L2dyYWRsZS1wbHVnaW4vJyB9CiAgICAgICAgbWF2ZW4geyB1cmwgJ2h0dHBzOi8vaml0cGFjay5pby8nIH0KICAgIH0KfQoKLy8gcGx1Z2luCnNldHRpbmdzRXZhbHVhdGVkIHsgc2V0dGluZ3MgLT4KICAgIHNldHRpbmdzLnBsdWdpbk1hbmFnZW1lbnQgewogICAgICAgIC8vIFByaW50IHJlcG9zaXRvcmllcyBjb2xsZWN0aW9uCiAgICAgICAgLy8gcHJpbnRsbiAiUmVwb3NpdG9yaWVzIG5hbWVzOiAiICsgcmVwb3NpdG9yaWVzLmdldE5hbWVzKCkKCiAgICAgICAgLy8gQ2xlYXIgcmVwb3NpdG9yaWVzIGNvbGxlY3Rpb24KICAgICAgICByZXBvc2l0b3JpZXMuY2xlYXIoKQoKICAgICAgICAvLyBBZGQgbXkgQXJ0aWZhY3RvcnkgbWlycm9yCiAgICAgICAgcmVwb3NpdG9yaWVzIHsKCSAgICBtYXZlbkxvY2FsKCkKICAgICAgICAgICAgbWF2ZW4gewogICAgICAgICAgICAgICAgdXJsICJodHRwczovL21hdmVuLmFsaXl1bi5jb20vcmVwb3NpdG9yeS9ncmFkbGUtcGx1Z2luLyIKICAgICAgICAgICAgfQogICAgICAgIH0KICAgIH0KfQo=\" | base64 --decode > ${GRADLE_CONFIG_DIR}/init.gradle\n```\n\n如上所示，在打包的最后环节：\n\n- 添加.gradle目录\n\n- 创建init.gradle脚本\n\n- 由于Dockerfile的语法格式限制，我们将配置文件编码为Base64再写入\n\n配置文件的原文如下：\n\n```groovy\n// project\nallprojects{\n    repositories {\n    mavenLocal()\n        maven { url 'https://maven.aliyun.com/repository/public/' }\n        maven { url 'https://maven.aliyun.com/repository/jcenter/' }\n        maven { url 'https://maven.aliyun.com/repository/google/' }\n        maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }\n        maven { url 'https://jitpack.io/' }\n    }\n}\n\n// plugin\nsettingsEvaluated { settings ->\n    settings.pluginManagement {\n        // Print repositories collection\n        // println \"Repositories names: \" + repositories.getNames()\n\n        // Clear repositories collection\n        repositories.clear()\n\n        // Add my Artifactory mirror\n        repositories {\n        mavenLocal()\n            maven {\n                url \"https://maven.aliyun.com/repository/gradle-plugin/\"\n            }\n        }\n    }\n}\n```\n\n我们使用新镜像重启Agent，会发现编译环节由1分钟缩短到10秒内。\n\n- ## 滚动升级\n\n在之前构建的版本中，我们只考虑了部署，没有考虑升级情况。\n\n可以修改JenkinsFile，采用\"yaml + kubectl apply\"的方式，让部署支持滚动升级。\n\n```groovy\npipeline {\n    agent any\n\n    environment {\n        project = \"coder4/homs-start\"\n    }\n\n    stages {\n        stage('git') {\n            steps {\n                git credentialsId: 'GITEE', url: 'git@gitee.com:/'+ project + '.git', branch: 'master'\n            }\n        }\n\n        stage('gradle') {\n            steps {\n                sh \"gradle build\"\n            }    \n        }\n\n        stage('docker image build') {\n            steps {\n                sh '''\n                # get right jar\n                jarPath=$(du -a ./build/libs/* | sort -n -r | head -n 1 | cut -f2-)\n                jarFile=$( echo ${jarPath##*/} )\n\n                # make Dockerfile\ncat <<EOF > Dockerfile\nFROM openjdk:8\nCOPY $jarPath $jarFile\nENTRYPOINT [\"java\",\"-jar\",\"/$jarFile\"]\nEOF\n                # build Docker image\n                sudo docker build -t coder4/${JOB_NAME}:${BUILD_NUMBER} .\n\n                # push to docker hub\n                sudo docker push coder4/${JOB_NAME}:${BUILD_NUMBER}\n                '''\n            }\n        }\n\n        stage('k8s') { \n            steps {\n                withKubeConfig([credentialsId: \"60a8e9d2-0212-4ff4-aa98-f46fced97121\",serverUrl: \"https://kubernetes:6443\"]) {\n                    sh \"\"\"\n                    # prepare deployment yaml\ncat <<EOF  | kubectl apply -f -\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: ${JOB_NAME}-deployment\n  labels:\n    app: ${JOB_NAME}\nspec:\n  selector:\n    matchLabels:\n      app: ${JOB_NAME}\n  replicas: 1\n  strategy:\n    type: RollingUpdate\n  template:\n    metadata:\n      labels:\n        app: ${JOB_NAME}\n    spec:\n      containers:\n        - name: ${JOB_NAME}-server\n          image: coder4/${JOB_NAME}:${BUILD_NUMBER}\n          ports:\n            - containerPort: 8080\n                    \"\"\"\n                }\n            }\n        }\n    }\n}\n```\n\n经过上述改造后，我们可以随时滚动升级新版本了。\n\n## 支持回滚操作\n\n在新版本发布后，可能会遇到故障，需要回滚的情况，这也需要流水线支持这一功能。\n\n我们采用\"Parameterized Project\"的方式，来设定参数。\n\n首先，修改当前项目的配置，勾选\"This project is parameterized\"。\n\n接着，安装插件“Active Choice”，以便开启Groovy脚本的“动态参数”。\n\n加下来，我们添加3个参数\n\n1. Active Choices Parameter，参数名\"JobName\"\n\n代码、截图如下：\n\n```groovy\nm = Thread.currentThread().toString() =~ /job\\/(.*)\\/build/\nreturn [m[0][1]]\n```\n\n![f](./jenkins-pipeline-param1.png)\n\n2. Choose Parameter，参数名\"Action\"，固定两个选项：Deploy、Rollback\n\n代码和截图如下：\n\n![f](./jenkins-pipeline-param1.png)\n\n3. Active Choices Reactive Parameter，参数名\"RollbackVersion\"\n\n需要配置Referenced parameters为\"Action,JobName\"\n\n代码和截图如下：\n\n```groovy\nif (Action.equals('Deploy')) {\n    return []\n} else {\n    return jenkins.model.Jenkins.instance.getJob(JobName).builds.findAll{ it.result == hudson.model.Result.SUCCESS }.collect{ \"$it.number\".toString() }\n}\n```\n\n![f](./jenkins-pipeline-param3.png)\n\n经过上述设置，我们的项目拥有了3个可输入参数，如下图所示：\n\n![f](./jenkins-build-with-parameters.png)\n\n其中：\n\nJobName：项目名\n\nAction：决定了是部署 or 回滚\n\nRollbackVersion：仅当回滚时生效，决定了要回滚到哪个版本\n\n除此之外，我们还需要对JenkinsFile进行改造，如下：\n\n```groovy\npipeline {\n    agent any\n\n    stages {\n        stage('git') {\n            steps {\n                script {\n                    if (params.Action.equals(\"Rollback\")) {\n                        echo \"Skip in Rollback\"\n                    } else {\n                        git credentialsId: 'GITEE', url: 'git@gitee.com:/coder4/'+ env.JOB_NAME + '.git', branch: 'master'\n                    }\n                }\n            }\n        }\n\n        stage('gradle') {\n            steps {\n                script {\n                    if (params.Action.equals(\"Rollback\")) {\n                        echo \"Skip in Rollback\"\n                    } else {\n                        sh \"gradle build\"\n                    }\n                }\n            }    \n        }\n\n        stage('docker image build') {\n            steps {\n                script {\n                    if (params.Action.equals(\"Rollback\")) {\n                        echo \"Skip in Rollback\"\n                    } else {\n                        sh '''\n                # get right jar\n                jarPath=$(du -a ./build/libs/* | sort -n -r | head -n 1 | cut -f2-)\n                jarFile=$( echo ${jarPath##*/} )\n\n                # make Dockerfile\ncat <<EOF > Dockerfile\nFROM openjdk:8\nCOPY $jarPath $jarFile\nENTRYPOINT [\"java\",\"-jar\",\"/$jarFile\"]\nEOF\n                # build Docker image\n                sudo docker build -t coder4/${JOB_NAME}:${BUILD_NUMBER} .\n\n                # push to docker hub\n                sudo docker push coder4/${JOB_NAME}:${BUILD_NUMBER}\n                '''\n                    }\n                }\n            }\n        }\n\n        stage('k8s') { \n            steps {\n                script {\n                    env.DEPLOY_VERSION = params.Action.equals(\"Rollback\") ? params.RollbackVersion : env.BUILD_NUMBER\n\n\n                    withKubeConfig([credentialsId: \"60a8e9d2-0212-4ff4-aa98-f46fced97121\",serverUrl: \"https://kubernetes:6443\"]) {\n                        sh \"\"\"\n\necho \"Kubernetes Deploy $JOB_NAME Version $DEPLOY_VERSION\"\n\n# prepare deployment yaml\ncat <<EOF  | kubectl apply -f -\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: ${JOB_NAME}-deployment\n  labels:\n    app: ${JOB_NAME}\nspec:\n  selector:\n    matchLabels:\n      app: ${JOB_NAME}\n  replicas: 1\n  strategy:\n    type: RollingUpdate\n  template:\n    metadata:\n      labels:\n        app: ${JOB_NAME}\n    spec:\n      containers:\n        - name: ${JOB_NAME}-server\n          image: coder4/${JOB_NAME}:${DEPLOY_VERSION}\n          ports:\n            - containerPort: 8080\n                    \"\"\"\n                    }\n                }\n            }\n        }\n    }\n}\n```\n\n我们来试验一下成果，首先，执行新部署：执行成功，版本号111，耗时21s\n\n```shell\nkubectl describe pod homs-start-deployment-644677f984-bksl9\nName:         homs-start-deployment-644677f984-bksl9\nNamespace:    default\nPriority:     0\nNode:         minikube/192.168.49.2\nStart Time:   Thu, 11 Nov 2021 19:06:25 +0800\nLabels:       app=homs-start\n              pod-template-hash=644677f984\nAnnotations:  <none>\nStatus:       Running\nIP:           172.17.0.4\nIPs:\n  IP:           172.17.0.4\nControlled By:  ReplicaSet/homs-start-deployment-644677f984\nContainers:\n  homs-start-server:\n    Container ID:   docker://279e11005931dfd8aa876134bb2441294a768766261aeb0bb88b5004047f5060\n    Image:          coder4/homs-start:111\n    Image ID:       docker-pullable://coder4/homs-start@sha256:526640caca84a10254e42ad12dd617eaf45c75c17b4ebb7731fe623509938e5c\n    Port:           8080/TCP\n    Host Port:      0/TCP\n    State:          Running\n      Started:      Thu, 11 Nov 2021 19:06:31 +0800\n    Ready:          True\n    Restart Count:  0\n    Environment:    <none>\n    Mounts:\n      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-gkpv7 (ro)\nConditions:\n  Type              Status\n  Initialized       True \n  Ready             True \n  ContainersReady   True \n  PodScheduled      True \nVolumes:\n  kube-api-access-gkpv7:\n    Type:                    Projected (a volume that contains injected data from multiple sources)\n    TokenExpirationSeconds:  3607\n    ConfigMapName:           kube-root-ca.crt\n    ConfigMapOptional:       <nil>\n    DownwardAPI:             true\nQoS Class:                   BestEffort\nNode-Selectors:              <none>\nTolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s\n                             node.kubernetes.io/unreachable:NoExecute op=Exists for 300s\nEvents:\n  Type    Reason     Age   From               Message\n  ----    ------     ----  ----               -------\n  Normal  Scheduled  37s   default-scheduler  Successfully assigned default/homs-start-deployment-644677f984-bksl9 to minikube\n  Normal  Pulling    37s   kubelet            Pulling image \"coder4/homs-start:111\"\n  Normal  Pulled     31s   kubelet            Successfully pulled image \"coder4/homs-start:111\" in 5.781019732s\n  Normal  Created    31s   kubelet            Created container homs-start-server\n  Normal  Started    31s   kubelet            Started container homs-start-server\n```\n\n接下来，我们回滚到107版本，由于机器上有镜像，因此只耗时1s。\n\n```shell\nkubectl describe pod homs-start-deployment-5bf947768c-dt8w2\nName:         homs-start-deployment-5bf947768c-dt8w2\nNamespace:    default\nPriority:     0\nNode:         minikube/192.168.49.2\nStart Time:   Thu, 11 Nov 2021 18:49:22 +0800\nLabels:       app=homs-start\n              pod-template-hash=5bf947768c\nAnnotations:  <none>\nStatus:       Running\nIP:           172.17.0.5\nIPs:\n  IP:           172.17.0.5\nControlled By:  ReplicaSet/homs-start-deployment-5bf947768c\nContainers:\n  homs-start-server:\n    Container ID:   docker://bc626494af343b6d56b707258e03a85ae668abb21dcc3ca2b72d6239e3b56b3d\n    Image:          coder4/homs-start:107\n    Image ID:       docker-pullable://coder4/homs-start@sha256:526640caca84a10254e42ad12dd617eaf45c75c17b4ebb7731fe623509938e5c\n    Port:           8080/TCP\n    Host Port:      0/TCP\n    State:          Running\n      Started:      Thu, 11 Nov 2021 18:49:27 +0800\n    Ready:          True\n    Restart Count:  0\n    Environment:    <none>\n    Mounts:\n      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-dt7g2 (ro)\nConditions:\n  Type              Status\n  Initialized       True \n  Ready             True \n  ContainersReady   True \n  PodScheduled      True \nVolumes:\n  kube-api-access-dt7g2:\n    Type:                    Projected (a volume that contains injected data from multiple sources)\n    TokenExpirationSeconds:  3607\n    ConfigMapName:           kube-root-ca.crt\n    ConfigMapOptional:       <nil>\n    DownwardAPI:             true\nQoS Class:                   BestEffort\nNode-Selectors:              <none>\nTolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s\n                             node.kubernetes.io/unreachable:NoExecute op=Exists for 300s\nEvents:\n  Type    Reason     Age   From               Message\n  ----    ------     ----  ----               -------\n  Normal  Scheduled  16m   default-scheduler  Successfully assigned default/homs-start-deployment-5bf947768c-dt8w2 to minikube\n  Normal  Pulling    16m   kubelet            Pulling image \"coder4/homs-start:107\"\n  Normal  Pulled     16m   kubelet            Successfully pulled image \"coder4/homs-start:107\" in 3.365201023s\n  Normal  Created    16m   kubelet            Created container homs-start-server\n  Normal  Started    16m   kubelet            Started container homs-start-server\n```\n\n在本文中，我们围绕编译、镜像进行了优化，但这还远没有达到\"完美\"的程度。\n\n我提一些思路，供大家参考：\n\n1. docker镜像瘦身：打Dokcer镜像时，其实无需将jdk+ jar包一起打，可以只打jar包。在生成Deployment时，通过Pod的init container模式，将jar包拷贝进jdk的运行容器中，从而完成启动。\n\n2. 回滚版本选择优化：在前面的实现中，我们筛选了所有成功部署过的版本，将其做为可回滚的版本，但这其中的一部分，实际是通过\"回滚\"的方式部署成功的，在镜像仓库中，并没有与之对应的镜像版本。我们可以拉取镜像仓库中可用的版本，来实现回滚。\n\n3. 镜像版本优化：目前采用的是Job的\"Build Version\"做为镜像版本，可以再此基础上，追加Git版本号，以便区分代码拉取。\n\n4. 支持多分之：当前，我们默认用的是master分之，应当可以通过参数的方式，支持不同分之的修改。\n\n5. JenkinsFile共享：目前的JenkinsFile是直接配置在项目中的，如果微服务项目很多，逐一配置势必很麻烦，可以通过 “Jenkins Shared Library”的方式，在多项目间共享脚本配置。\n"
  },
  {
    "path": "src/ch06-cd/jenkins-k8s.md",
    "content": "# Jenkins实现Kubernetes部署流水线\n\n在Agent定制环境准备好后，我们将构建完整的部署流水线。\n\n根据我们选用的技术栈，部署流水线划分为如下阶段：\n\n1. checkout代码\n\n2. gradle编译\n\n3. 构建Docker镜像、推送到镜像服务器\n\n4. 发布到Kubernetes中\n\n在开始构建流水线前，我们还需要做一些准备工作。\n\n## 准备工作\n\n首先，我们需要创建一个新的Spring Boot项目homs-start，用于流水线的演示。\n\n这里使用Sping Boot Starter直接生成的，代码放到了Gitee托管，参考[这里](https://gitee.com/coder4/homs-start)。\n\n第二步，我们需要修改Jenskin的项目名，从test修改为homs-start。\n\n接下来，我们需要在Jenkins上配置Gitee的ssh key凭据。\n\n1. 先确认已在Gitee上配置了公钥，并且保留了对应的私钥，参考[这篇教程](https://gitee.com/help/articles/4181)。\n\n2. 在Jenkins上配置Gitee的凭据，路径是：Jenkins -> Manage Jenkins -> Manage Credentials -> Global\n\n3. SSH Username with private key，填入gitee的用户名和对应私钥，命名为GITEE\n\n![f](./jenkins-gitee-credential.png)\n\n在流水线的步骤3中，我们需要打包一个新的镜像。\n\n如果你还记得前两节的内容，应该知道我们的Agent实际是运行在Docker中的。\n\n因此，我们的Agent需要具有\"Docker Inside Docker\"的能力，一般常见的有三种方法，可以参考[这篇文章]([如何在Docker容器中运行Docker [3种方法] - 云+社区 - 腾讯云](https://cloud.tencent.com/developer/article/1697053))。\n\n本文中，我们选用socks挂载的模式，对Agent的镜像做一些改造，如下：\n\n```shell\nFROM jenkins/inbound-agent:latest-jdk8\n\nENV GRADLE_VERSION=7.2\nENV K8S_VERSION=v1.22.3\nENV DOCKER_CHANNEL stable\nENV DOCKER_VERSION 18.06.3-ce \n\n# tool\nUSER root\nRUN apt-get update && \\\n    apt-get install -y curl unzip sudo && \\\n    apt-get clean\n\n# docker\nRUN curl -fsSL \"https://download.docker.com/linux/static/${DOCKER_CHANNEL}/x86_64/docker-${DOCKER_VERSION}.tgz\" \\\n  | tar -xzC /usr/local/bin --strip=1 docker/docker\n\n# gradle\nRUN curl -skL -o /tmp/gradle-bin.zip https://services.gradle.org/distributions/gradle-$GRADLE_VERSION-bin.zip && \\\n    mkdir -p /opt/gradle && \\\n    unzip -q /tmp/gradle-bin.zip -d /opt/gradle && \\\n    ln -sf /opt/gradle/gradle-$GRADLE_VERSION/bin/gradle /usr/local/bin/gradle\n\nRUN chown -R 1001:0 /opt/gradle && \\\n    chmod -R g+rw /opt/gradle\n\n# kubectl\nRUN curl -LO https://storage.googleapis.com/kubernetes-release/release/$K8S_VERSION/bin/linux/amd64/kubectl\nRUN chmod +x ./kubectl\nRUN mv ./kubectl /usr/local/bin\n\n# add jenkins user to sudoer without password \nRUN usermod -aG sudo jenkins \nRUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers\n\nUSER jenkins\n```\n\n如上所述，我们对构建镜像的改动如下：\n\n- 增加了docker二进制文件\n\n- 对用户jenkins添加了sudo免密权限\n\n运行脚本也需要做一些改造：\n\n```shell\n#!/bin/bash\n\nNAME=\"jenkins_e1\"\nPUID=\"1000\"\nPGID=\"1000\"\n\ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --name $NAME \\\n    --env PUID=$PUID \\\n    --env PGID=$PGID \\\n    --add-host kubernetes:10.1.172.136 \\\n    --volume /var/run/docker.sock:/var/run/docker.sock \\\n    --detach \\\n    --init coder4/jenkins-my-agent \\\n    -workDir=/home/jenkins/agent \\\n    -url http://10.1.172.136:8080 \\\n    b057970bf978f53a8f945d470ac644e44c945e4b7259b216f703dedb95d0cac9 \\\n    e1\n```\n\n运行脚本的主要是，挂载了/var/run/docker.sock到容器内。\n\n运行后，我们以默认用户登录到容器内，查看docker是否可以正常使用：\n\n```shell\njenkins@936e27b3c460:~$ sudo docker ps\nCONTAINER ID        IMAGE                                                                 COMMAND                  CREATED             STATUS              PORTS                                                                                                                                  NAMES\n936e27b3c460        coder4/jenkins-my-agent                                               \"/usr/local/bin/jenk…\"   6 seconds ago       Up 4 seconds                                                                                                                                               jenkins_e1\n577db2106c7d        jenkins/jenkins:lts-jdk11                                             \"/sbin/tini -- /usr/…\"   4 days ago          Up About an hour    0.0.0.0:8080->8080/tcp, :::8080->8080/tcp, 0.0.0.0:50000->50000/tcp, :::50000->50000/tcp                                               jenkins\nd44c3e421fb7        registry.cn-hangzhou.aliyuncs.com/google_containers/kicbase:v0.0.25   \"/usr/local/bin/entr…\"   5 days ago          Up About an hour    127.0.0.1:50437->22/tcp, 127.0.0.1:50440->2376/tcp, 127.0.0.1:50442->5000/tcp, 127.0.0.1:50443->8443/tcp, 127.0.0.1:50441->32443/tcp   minikube\n```\n\n注意，因为挂载的socks默认是root权限，这里需要使用sudo。\n\n## 构建脚本\n\n下面，我们按照流水线的步骤，构建脚本如下：\n\n```groovy\npipeline {\n    agent any\n\n    environment {\n        project = \"coder4/homs-start\"\n    }\n\n    stages {\n        stage('git') {\n            steps {\n                git credentialsId: 'GITEE', url: 'git@gitee.com:/'+ project + '.git', branch: 'master'\n            }\n        }\n\n        stage('gradle') {\n            steps {\n                sh \"gradle build\"\n            }    \n        }\n\n        stage('docker image build') {\n            steps {\n                sh '''\n                # get right jar\n                jarPath=$(du -a ./build/libs/* | sort -n -r | head -n 1 | cut -f2-)\n                jarFile=$( echo ${jarPath##*/} )\n\n                # make Dockerfile\ncat <<EOF > Dockerfile\nFROM openjdk:8\nCOPY $jarPath $jarFile\nENTRYPOINT [\"java\",\"-jar\",\"/$jarFile\"]\nEOF\n                # build Docker image\n                sudo docker build -t coder4/${JOB_NAME}:${BUILD_NUMBER} .\n\n                # push to docker hub\n                sudo docker push coder4/${JOB_NAME}:${BUILD_NUMBER}\n                '''\n            }\n        }\n\n        stage('k8s') { \n            steps {\n                withKubeConfig([credentialsId: \"60a8e9d2-0212-4ff4-aa98-f46fced97121\",serverUrl: \"https://kubernetes:6443\"]) {\n                    sh \"kubectl create deployment my-nginx --image=coder4/${JOB_NAME}:${BUILD_NUMBER}\"\n                }\n            }\n        }\n    }\n}\n```\n\n脚本比较长，我们分步解析：\n\n1. git拉代码\n   \n   1. 这里直接使用的gitee的公开仓库，可以根据实际情况，替换为公司内的gitlab等私有仓库\n   \n   2. GITEE的凭据，就是在准备工作中配置的那个\n\n2. gradle编译\n   \n   1. 这里直接使用gradle build命令\n   \n   2. 编译好后，会在build/libs目录下，生成jar包\n\n3. 打包Docker镜像，上传镜像\n   \n   1. 首先选择build/libs下尺寸最大的jar包(一般是fat jar，可独立运行的那个)\n   \n   2. 基于openjdk8的基础镜像，添加打好的jar包，并设定启动为jar包\n   \n   3. 构建好镜像后，将其推送到镜像仓库。这里选用了Docker Hub共有仓库，你可以换用Harbor等私有仓库。\n   \n   4. 这里默认使用项目名做为镜像名，构建版本做为镜像版本号\n\n4. 在Kubernetes上部署\n   \n   1. 使用上面的镜像，创建一个deployment\n\n保存上述JenkinsFile脚本后，点击部署，如果一切顺利，会部署成功，我们看一下部署结果：\n\n```shell\nkubectl get pods\nNAME                        READY   STATUS              RESTARTS   AGE\nhoms-start795f967dd6-7szxp   1/1     Running   0          57s\n```\n\n查看日志：\n\n```shell\nkubectl logs -f my-nginx-795f967dd6-7szxp\n\n  .   ____          _            __ _ _\n /\\\\ / ___'_ __ _ _(_)_ __  __ _ \\ \\ \\ \\\n( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\\n \\\\/  ___)| |_)| | | | | || (_| |  ) ) ) )\n  '  |____| .__|_| |_|_| |_\\__, | / / / /\n =========|_|==============|___/=/_/_/_/\n :: Spring Boot ::                (v2.5.6)\n\n2021-11-10 02:49:45.469  INFO 1 --- [           main] com.homs.start.StartApplication          : Starting StartApplication using Java 1.8.0_312 on my-nginx-795f967dd6-7szxp with PID 1 (/homs-start-0.0.1-SNAPSHOT.jar started by root in /)\n2021-11-10 02:49:45.473  INFO 1 --- [           main] com.homs.start.StartApplication          : No active profile set, falling back to default profiles: default\n2021-11-10 02:49:46.866  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)\n2021-11-10 02:49:46.887  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]\n2021-11-10 02:49:46.887  INFO 1 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.54]\n2021-11-10 02:49:46.999  INFO 1 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext\n2021-11-10 02:49:47.000  INFO 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1450 ms\n2021-11-10 02:49:47.964  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''\n2021-11-10 02:49:47.974  INFO 1 --- [           main] com.homs.start.StartApplication          : Started StartApplication in 3.119 seconds (JVM running for 5.216)\n```\n\n如上所示，Pod中的Spring Boot进程已成功启动！\n\n至此，我们已经完整地实现了全链路的部署流水线开发。\n\n同时，上述流水线还有很大的改进空间，我们将在下一节继续优化流水线。\n"
  },
  {
    "path": "src/ch06-cd/jenkins.md",
    "content": "## Jenkins搭建入门\n\nJenkins是一款开源、强大的持续集成工具，其前身是Hudson(商用软件)。\n\n本节将介绍Jenkins的搭建。从架构上理解，Jenklins由两类角色组成：\n\n- Controller：主控节点，负责管理、配置工作，也称作Master节点。\n\n- Agent：执行具体作业的工作节点，也称作Slave节点，或者Executor节点。\n\n严格来说，Master节点也可以执行具体作业，但是处于安全性考虑，不建议这样做。\n\n## Jeknins的启动与初始配置\n\n首先启动Controller节点：\n\n```bash\n#!/bin/bash\n\nNAME=\"jenkins\"\nPUID=\"1000\"\nPGID=\"1000\"\n\nVOLUME=\"$HOME/docker_data/jenkins\"\nmkdir -p $VOLUME \n\ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    -v $VOLUME:/var/jenkins_home \\\n    -p 8080:8080 \\\n    -p 50000:50000 \\\n    --detach \\\n    --restart always \\\n    jenkins/jenkins:lts-jdk11\n```\n\n如上所示，我们启动了jenkins的主控节点，并对外暴露了8080、5000两个端口。\n\n我们在浏览器中打开如下链接：http://127.0.0.1:8080/\n\n![f](./jenkins-install.png)\n\n第一次启动会进行初始化，要求输入密码，我们使用如下命令查看：\n\n```shell\ndocker logs -f jenkins\n\n....\n\n*************************************************************\n*************************************************************\n*************************************************************\n\nJenkins initial setup is required. An admin user has been created and a password generated.\nPlease use the following password to proceed to installation:\n\n9169c97282d64545b36bc96cf7c1aaab\n\nThis may also be found at: /var/jenkins_home/secrets/initialAdminPassword\n\n*************************************************************\n*************************************************************\n*************************************************************\n\n2021-11-04 03:15:53.502+0000 [id=49]    INFO    h.m.DownloadService$Downloadable#load: Obtained the updated data file for hudson.tasks.Maven.MavenInstaller\n2021-11-04 03:15:53.502+0000 [id=49]    INFO    hudson.util.Retrier#start: Performed the action check updates server successfully at the attempt #1\n2021-11-04 03:15:53.517+0000 [id=49]    INFO    hudson.model.AsyncPeriodicWork#lambda$doRun$0: Finished Download metadata. 36,815 ms码\n```\n\n如上中间部分，即初始密码。\n\n输入初始密码后，会要求安装创建，建议至少安装下述插件：\n\n- Gradle：用于Java项目的打包和编译\n\n- Pipeline：用户开发流水线作业\n\n- Git：用于代码拉取\n\n- SSH Build Agents\n\n- Kubernetes：用于在Kubernetes集群上启动Slave节点\n\n- Kubernetes CLI：用于执行远程Kubernetes的二进制文件\n\n安装完插件后，需要创建初始管理员账号。\n\n## Jeknins的Agent节点配置\n\n启动Controller节点后，我们着手配置Slave节点，这里也有多种选项：\n\n- 启动固定数量的Slave节点\n\n- 按需启动，用完释放\n\n- 上述两种方案的混合\n\n考虑到并发性、资源利用率，我们选用方案2：在Kubernetes集群上，按需启动Slave容器，执行完毕后销毁。\n\n首先，我们需要登录到Kubernetes集群的Master节点上，查看已有的证书信息。\n\n```shell\ncd ~/.kube/config\n\napiVersion: v1\nclusters:\n- cluster:\n    certificate-authority: /Users/coder4/.minikube/ca.crt\n    extensions:\n    - extension:\n        last-update: Thu, 04 Nov 2021 11:23:17 CST\n        provider: minikube.sigs.k8s.io\n        version: v1.22.0\n      name: cluster_info\n    server: https://127.0.0.1:52058\n  name: minikube\ncontexts:\n- context:\n    cluster: minikube\n    extensions:\n    - extension:\n        last-update: Thu, 04 Nov 2021 11:23:17 CST\n        provider: minikube.sigs.k8s.io\n        version: v1.22.0\n      name: context_info\n    namespace: default\n    user: minikube\n  name: minikube\ncurrent-context: minikube\nkind: Config\npreferences: {}\nusers:\n- name: minikube\n  user:\n    client-certificate: /Users/coder4/.minikube/profiles/minikube/client.crt\n    client-key: /Users/coder4/.minikube/profiles/minikube/client.key\n```\n\n如上，共包含了3个证书/密钥：ca.crt、client.crt、client.key。\n\n我们使用他们创建新的凭据，供Jenkins使用：\n\n```shell\nopenssl pkcs12 -export -out ./kube-jenkins.pfx -inkey ./client.key -in ./client.crt -certfile ./ca.crt\n```\n\n上述创建过程会要求输入密码，请记牢后续会用到。\n\n此外，上述文件中的ca.crt后面会再次用到。\n\n在Jenkins上配置Kubernetes集群之前，我们假设以下信息：\n\n- 10.1.172.136：Jenkins所在的物理机节点\n\n- https://127.0.0.1:52058：Kubernetes集群的api server地址\n\n由于我当前使用的minikube，不难发现，minikube的api server只在本地开了端口，并没有监听到物理机上，因此网段是不通的，所以我们先使用socat进行端口映射。\n\n```shell\nsocat TCP4-LISTEN:6443,fork TCP4:127.0.0.1:52058\n```\n\n如上，经过映射后，所有打到本机的公网IP(10.1.172.136)、端口6443上的流量，会被自动转发到52058上。\n\n接下来，我们着手在Jenkins上添加Kubernetes的集群配置。\n\n Manage Jenkins -> Manage Nodes and Clouds -> Configure Clouds -> Add a new cloud -> Kubernetes\n\n截图如下：\n\n![f](./jenkins-k8s-first.png)\n\n其中核心配置如下：\n\n- 名称：自选必填，这里选了kubernetes\n\n- Kuberenetes地址：https://10.1.172.136:6443\n\n- Kubernetes 服务证书 key：输入上文中ca.crt中的信息，注意换行问题。\n\n- 凭据：上传上述生成的kube-jenkins.pfx，同时输入密码\n\n- Jenkins地址：http://10.1.172.136:8080\n\n上述天禧后，点击\"连接测试\"，如果一切正常，你会发现如下报错：\n\n![f](./jenkins-host-invalid.png)\n\n这是因为我们经过转发后，host与证书中的并不匹配。\n\n我们修改下Jenkins的docker启动脚本，添加hosts参数：\n\n```shell\n--add-host kubernetes:10.1.172.136\n```\n\n重启Jenkins后，将上述位置的\"Kuberenetes地址\"修改为\"https://kubernetes:6443\"，再次重试连接，一切会成功。\n\n记得保存所有配置。\n\n## 测试任务\n\n我们配置一个测试任务：\n\n新建任务 -> 流水线\n\n代码如下：\n\n```groovy\npodTemplate {\n    node(POD_LABEL) {\n        stage('Run shell') {\n            sh 'echo hello world'\n        }\n    }\n}\n```\n\n保存后，点击\"立即构建\"，运行结果如下：\n\n```shell\nStarted by user admin\n[Pipeline] Start of Pipeline\n[Pipeline] podTemplate\n[Pipeline] {\n[Pipeline] node\nCreated Pod: kubernetes default/test-4-xsc01-4292c-4rkrz\n[Normal][default/test-4-xsc01-4292c-4rkrz][Scheduled] Successfully assigned default/test-4-xsc01-4292c-4rkrz to minikube\n[Normal][default/test-4-xsc01-4292c-4rkrz][Pulled] Container image \"jenkins/inbound-agent:4.3-4-jdk11\" already present on machine\n[Normal][default/test-4-xsc01-4292c-4rkrz][Created] Created container jnlp\n[Normal][default/test-4-xsc01-4292c-4rkrz][Started] Started container jnlp\nAgent test-4-xsc01-4292c-4rkrz is provisioned from template test_4-xsc01-4292c\n---\napiVersion: \"v1\"\nkind: \"Pod\"\nmetadata:\n  annotations:\n    buildUrl: \"http://10.1.172.136:8080/job/test/4/\"\n    runUrl: \"job/test/4/\"\n  labels:\n    jenkins: \"slave\"\n    jenkins/label-digest: \"802a637918cdcb746f1931e3fa50c8f991b59203\"\n    jenkins/label: \"test_4-xsc01\"\n  name: \"test-4-xsc01-4292c-4rkrz\"\nspec:\n  containers:\n  - env:\n    - name: \"JENKINS_SECRET\"\n      value: \"********\"\n    - name: \"JENKINS_AGENT_NAME\"\n      value: \"test-4-xsc01-4292c-4rkrz\"\n    - name: \"JENKINS_NAME\"\n      value: \"test-4-xsc01-4292c-4rkrz\"\n    - name: \"JENKINS_AGENT_WORKDIR\"\n      value: \"/home/jenkins/agent\"\n    - name: \"JENKINS_URL\"\n      value: \"http://10.1.172.136:8080/\"\n    image: \"jenkins/inbound-agent:4.3-4-jdk11\"\n    name: \"jnlp\"\n    resources:\n      limits: {}\n      requests:\n        memory: \"256Mi\"\n        cpu: \"100m\"\n    volumeMounts:\n    - mountPath: \"/home/jenkins/agent\"\n      name: \"workspace-volume\"\n      readOnly: false\n  nodeSelector:\n    kubernetes.io/os: \"linux\"\n  restartPolicy: \"Never\"\n  volumes:\n  - emptyDir:\n      medium: \"\"\n    name: \"workspace-volume\"\n\nRunning on test-4-xsc01-4292c-4rkrz in /home/jenkins/agent/workspace/test\n[Pipeline] {\n[Pipeline] stage\n[Pipeline] { (Run shell)\n[Pipeline] sh\n+ echo hello world\nhello world\n[Pipeline] }\n[Pipeline] // stage\n[Pipeline] }\n[Pipeline] // node\n[Pipeline] }\n[Pipeline] // podTemplate\n[Pipeline] End of Pipeline\nFinished: SUCCESS\n```\n\n至此，我们已经成功配置了基础的Jenkins，并成功在Kubernetes集群上执行了一次构建任务。\n"
  },
  {
    "path": "src/ch07-tools/.md",
    "content": ""
  },
  {
    "path": "src/ch07-tools/README.md",
    "content": "# 工具链\n\n微服务架构的成功落地，离不开工具链的辅助。\n\n本节将讨论与研发密切相关的工具链，包括\n\n1. 快速生成微服务的模板工具\n\n2. Ldap及内网认证系统\n\n3. 基于Gitlab的私有代码平台\n\n4. 基于JFrog Artifactory的Maven私有仓库\n\n5. 基于Registry 2的Docker镜像私有仓库\n\n如果你有好工具推荐，请提Issue告诉我 : - )\n"
  },
  {
    "path": "src/ch07-tools/gitlab.md",
    "content": "# 基于Gitlab搭建版本控制平台\n\n做为程序员，你一定使用过GitHub / Gitee等开源代码仓库。\n\n对于公司而言，直接将代码上传到开源仓库，对所有用户公开，会面临诸多问题：\n\n- 泄露商业机密\n\n- 安全漏洞泄露\n\n- 被抄袭、盗版\n\n因此，在公司内自建一套私有的代码仓库，是十分必要的。\n\n本节，我们将基于Gitlab，搭建私有的版本控制系统。\n\n## 运行\n\n我们使用Docker版本启动，脚本如下：\n\n```shell\n#!/bin/bash\n\nNAME=\"gitlab\"\nPUID=\"1000\"\nPGID=\"1000\"\n\nVOLUME=\"$HOME/docker_data/gitlab\"\nmkdir -p $VOLUME/{data,logs,config} \n\ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    --volume \"$VOLUME/config\":/etc/gitlab \\\n    --volume \"$VOLUME/logs\":/var/log/gitlab \\\n    --volume \"$VOLUME/data\":/var/opt/gitlab \\\n    --env PUID=$PUID \\\n    --env PGID=$PGID \\\n    -p 8888:80 \\\n    -p 10022:22 \\\n    --detach \\\n    --restart always \\\n    gitlab/gitlab-ce:14.1.8-ce.0\n```\n\n解释一下：\n\n- 上述开放了两个端口，8888和22\n\n- gitlab的配置放置于3个不同的位置，我们分别设置了Volume\n\n- 由于该镜像内置了多个进程，启动时间会比较久\n\n启动后，我们首先查看初始管理员密码，在config/initial_root_password文件中，只会保留24小时：\n\n```shell\n# WARNING: This value is valid only in the following conditions\n#          1. If provided manually (either via `GITLAB_ROOT_PASSWORD` environment variable or via `gitlab_rails['initial_root_password']` setting in `gitlab.rb`, it was provided before database was seeded for the first time (usually, the first reconfigure run).\n#          2. Password hasn't been changed manually, either via UI or via command line.\n#\n#          If the password shown here doesn't work, you must reset the admin password following https://docs.gitlab.com/ee/security/reset_user_password.html#reset-your-root-password.\n\nPassword: Sgh1UigBM6ht5ApoW1z2N4JOLHFoivK/EwpQwZ1PylI=\n\n# NOTE: This file will be automatically deleted in the first reconfigure run after 24 hours.\n```\n\n## 配置\n\n首先，修改conf/gitlab.rb，修改host，这里可以命名为实际的IP和端口：\n\n```shell\nexternal_url 'http://10.1.172.136:8888'\n```\n\n下一步，修改conf/gitlab.rb，添加ldap配置：\n\n```ruby\ngitlab_rails['ldap_enabled'] = true\ngitlab_rails['prevent_ldap_sign_in'] = false\n\ngitlab_rails['ldap_servers'] = YAML.load <<-'EOS'\n  main:\n    label: 'LDAP'\n    host: '10.1.172.136'\n    port: 389\n    uid: 'cn'\n    bind_dn: 'cn=readonly,dc=coder4,dc=com'\n    password: 'readonly123'\n    encryption: 'plain' # \"start_tls\" or \"simple_tls\" or \"plain\"\n    verify_certificates: false\n    smartcard_auth: false\n    active_directory: false\n    allow_username_or_email_login: false\n    lowercase_usernames: false\n    block_auto_created_users: false\n    base: 'ou=rd,dc=coder4,dc=com'\n    user_filter: ''\n    attributes: \n      username: ['cn']\n      email: ['mail']\n      name: cn\n    ## EE only\n    group_base: ''\n    admin_group: ''\n    sync_ssh_keys: false\nEOS\n```\n\n上面的配置信息，与我们在[LDAP](./ldap.md)中的设置的信息维持一致，请根据需要自行修改。\n\n重新应用配置，并重启：\n\n```shell\ndocker exec -it gitlab /bin/bash\n./bin/gitlab-ctl reconfigure\n```\n\n![f](./gitlab-ldap.png)\n\n重启后，我们使用LDAP中配置的zhangsan / 123456进行登录，成功！\n\n搭建Gitlab只是起点，你还应熟悉基本用法、开发模式，推荐如下文章：\n\n- [安装和使用GitLab]([安装和使用GitLab - 云服务器 ECS - 阿里云](https://help.aliyun.com/document_detail/52857.html))\n\n- [Creating merge requests]([Creating merge requests | GitLab](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html))\n"
  },
  {
    "path": "src/ch07-tools/jfrog-artifactory.md",
    "content": "# JFrog Artifactory搭建Maven私有仓库\n\n在本书技术架构中，我们选用了Gradle做为Java的依赖管理工具。\n\n实际上，Gradle只提供了构建的前端，实际使用的还是Maven仓库。\n\n出于安全性、速度等因素的考量，我们需要配置私有的Maven仓库。\n\n本节，我们将基于JFrog Artifactory，搭建私有的Maven仓库。\n\n## 运行\n\n启动脚本如下：\n\n```shell\n#!/bin/bash\n\nNAME=\"artifactory\"\nPUID=\"1000\"\nPGID=\"1000\"\n\nVOLUME=\"$HOME/docker_data/artifactory\"\nmkdir -p $VOLUME \n\ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    --volume $VOLUME:/var/opt/jfrog/artifactory \\\n    --env PUID=$PUID \\\n    --env PGID=$PGID \\\n    -p 8081:8081 \\\n    -p 8082:8082 \\\n    --detach \\\n    --restart always \\\n    releases-docker.jfrog.io/jfrog/artifactory-oss:latest\n```\n\n启动的时间会略长，可以访问 http://127.0.0.1:8082/ui/ 登录，默认的用户名是admin，密码是password。\n\n## 配置\n\n我们首先设置ldap关联，打开如下菜单：Administration -> Security -> LDAP -> Add Setting\n\n- LDAP URL：ldap://10.1.172.136:389/dc=coder4,dc=com\n\n- User DN Pattern：cn={0},ou=rd\n\n- Manager DN：cn=readonly,dc=coder4,dc=com\n\n- Manager Password：readonly123\n\n如下图所示：\n\n![f](./artifactory-ldap.png)\n\n接着，我们打开匿名访问权限(默认是关闭的)，位于：\n\nAdministration -> Security -> Security onfiguration  \n选中Allow Anonymous Access，然后点击保存。如下图所示\n\n![f](./artifactory-anoymous-access.png)\n\n最后，我们新建一个仓库：\n\nRepositories -> Add Repositories -> Local Repositories\n\n- Key：homs-release / homs-snapshot\n\n- Package Type：Gradle\n\n- Repository Key：homs\n\n- Handle Releases / Handle Snapshots\n\n同时，需要给用户配置权限：\n\n- Application -> Regisigrition -> Artifacts\n\n至此，仓库侧的配置已经完成。\n\n## 使用私有Maven仓库\n\n首先，添加本地全局配置：\n\n修改文件：~/.gradle/gradle.properties\n\n```groovy\nmavenReleaseRepo=http://127.0.0.1:8082/artifactory/homs-release/\nmavenSnapshotRepo=http://127.0.0.1:8082/artifactory/homs-snapshot/\nmavenUsername=zhangsan\nmavenPassword=123456\n```\n\n上述配置了私有仓库地址，测试和发布是分开的\n\n接下来，我们在项目中修改\n\nhoms-demo/build.gradle：\n\n```groovy\nplugins {\n\n  id 'java'\n  id 'idea'\n  id 'org.springframework.boot' version '2.5.3' apply false\n  id 'io.spring.dependency-management' version '1.0.11.RELEASE' apply false\n  id \"io.freefair.lombok\" version \"6.1.0\" apply false\n\n}\n\nsubprojects {\n\n  group = 'com.coder4'\n  version = '0.0.1-SNAPSHOT'\n  sourceCompatibility = '1.8'\n\n  apply plugin: 'java'\n  apply plugin: 'maven-publish'\n\n  publishing {\n    publications {\n      \"$project.name\"(MavenPublication) {\n        groupId project.group\n        artifactId project.name\n        version project.version\n        from components.java\n      }\n    }\n\n    repositories {\n      maven {\n        credentials {\n          username mavenUsername \n          password mavenPassword\n        }\n        url = version.endsWith('SNAPSHOT') ? mavenSnapshotRepo : mavenReleaseRepo\n      }\n    }\n  }\n\n}\n```\n\n如上所示，我们修改了主项目中的配置\n\n- publising会为每个子项目添加发布任务\n\n- repositoreis指定了发布的私有仓库地址\n\n在子项目中，我们需要略作修改，如下：\n\nhoms-client/build.gradle\n\n```groovy\nplugins {\n    id 'java'\n    // id 'io.spring.dependency-management'\n}\n\ndependencies {\n    implementation platform('com.coder4:bom-homs:1.0')\n    implementation platform('org.springframework.boot:spring-boot-dependencies:2.5.3')\n\n    implementation 'com.google.protobuf:protobuf-java'\n    implementation \"io.grpc:grpc-stub\"\n    implementation \"io.grpc:grpc-protobuf\"\n    implementation 'io.grpc:grpc-netty-shaded'\n\n    implementation \"org.slf4j:slf4j-api\"\n\n    implementation 'com.alibaba.nacos:nacos-client:2.0.3'\n    implementation 'org.springframework.boot:spring-boot-autoconfigure:2.2.0.RELEASE'\n\n}\n```\n\n上述修改，去掉了spring dependency这个插件，转而使用platform模式。\n\n这是一个spring + maven-publish插件共同使用导致的bug，建议都用platform来解决。\n\n最后，我们尝试发布：\n\n```shell\ngradle publish\n\n> Task :publishHoms-demo-clientPublicationToMaven2Repository\nCannot upload checksum for snapshot-maven-metadata.xml because the remote repository doesn't support SHA-512. This will not fail the build.\nCannot upload checksum for module-maven-metadata.xml because the remote repository doesn't support SHA-512. This will not fail the build.\n\n> Task :publishHoms-demo-serverPublicationToMaven2Repository\nCannot upload checksum for snapshot-maven-metadata.xml because the remote repository doesn't support SHA-512. This will not fail the build.\nCannot upload checksum for module-maven-metadata.xml because the remote repository doesn't support SHA-512. This will not fail the build.\n\nBUILD SUCCESSFUL in 2s\n4 actionable tasks: 4 executed\n```\n\n成功！\n\n## 引入依赖\n\n首先配置全局依赖：\n\n～/.gradle/init.gradle\n\n```groovy\n// project\nallprojects{\n    repositories {\n    mavenLocal()\n        maven { url mavenSnapshotRepo }\n        maven { url mavenReleaseRepo }\n        maven { url 'https://maven.aliyun.com/repository/public/' }\n        maven { url 'https://maven.aliyun.com/repository/jcenter/' }\n        maven { url 'https://maven.aliyun.com/repository/google/' }\n        maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }\n        maven { url 'https://jitpack.io/' }\n    }\n}\n\n// plugin\nsettingsEvaluated { settings ->\n    settings.pluginManagement {\n        // Print repositories collection\n        // println \"Repositories names: \" + repositories.getNames()\n\n        // Clear repositories collection\n        repositories.clear()\n\n        // Add my Artifactory mirror\n        repositories {\n        mavenLocal()\n            maven {\n                url \"https://maven.aliyun.com/repository/gradle-plugin/\"\n            }\n        }\n    }\n}\n```\n\n在下游中修改homs-start中添加依赖，和往常一样：\n\n```groovy\nplugins {\n    id 'org.springframework.boot' version '2.5.6'\n    id 'io.spring.dependency-management' version '1.0.11.RELEASE'\n    id 'java'\n    id 'maven-publish'\n}\n\ngroup = 'com.homs'\nversion = '0.0.1-SNAPSHOT'\nsourceCompatibility = '1.8'\n\nrepositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation 'org.springframework.boot:spring-boot-starter-web'\n    testImplementation 'org.springframework.boot:spring-boot-starter-test'\n    implementation 'com.coder4:homs-demo-client:0.0.1-SNAPSHOT'\n}\n\ntest {\n    useJUnitPlatform()\n}\n```\n\n尝试构建，成功！\n\n```shell\ngradle build             \n\nBUILD SUCCESSFUL in 7s\n```\n\n至此，我们成功引入了基于私有Maven仓库。\n"
  },
  {
    "path": "src/ch07-tools/ldap.md",
    "content": "# 基于LDAP的内网统一认证\n\n对于任何公司而言，一套“内部通用”的统一认证系统是必不可少的。\n\n请注意两个关键字：内部、通用。\n\n- 内部：认证系统只在公司内部关联的系统使用，并且需要关联具体的员工信息，如：工号、用户名、邮箱等。\n\n- 通用：这套系统不是只提供验证，还要和其他系统共享认证，例如：项目管理系统、版本控制系统、发布系统等等。\n\n在本书中，我们选取LDAP(Lightweight Directory Access Protocol)做为统一认证工具。\n\nLDAP是一个开放的，中立的，工业标准的应用协议，通过IP协议提供访问控制和维护分布式信息的目录信息。\n\n由于LDAP出现的年代比较久远(1993)，也并非专门为公司认证设计的，因此其易用性较差。我们选用[LDAP Account Manager](https://www.ldap-account-manager.org/)做为辅助管理工具。\n\n## 部署open-ldap服务\n\n我们选用开源的open-ldap做为服务端，进行部署：\n\n```shell\n#!/bin/bash\n\nNAME=\"openldap\"\nPUID=\"1000\"\nPGID=\"1000\"\n\nVOLUME=\"$HOME/docker_data/openldap/\"\nmkdir -p $VOLUME \n\ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    --volume \"$VOLUME:/data/openldap/\" \\\n    -e PUID=$PUID \\\n    -e PGID=$PGID \\\n    -e LDAP_TLS=false \\\n    -e LDAP_DOMAIN=coder4.com \\\n    -e LDAP_ADMIN_PASSWORD=admin123 \\\n    -e LDAP_CONFIG_PASSWORD=config123 \\\n    -e LDAP_READONLY_USER=true \\\n    -e LDAP_READONLY_USER_USERNAME=readonly \\\n    -e LDAP_READONLY_USER_PASSWORD=readonly123 \\\n    -p 389:389 \\\n    -p 636:636 \\\n    --detach \\\n    --restart always \\\n    osixia/openldap:1.5.0\n```\n\n如上所示：\n\n- 关闭了TLS加密，在生产环境中，建议配置证书并打开它\n\n- 域名：coder4.com，可以根据需要自行更改，会影响用户的后缀\n\n- 管理员密码：admin123，请根据需要自行更改\n\n- 配置用户密码：config123，请根据需要自行更改\n\n- 只读用户：readonly/readlony123，可自行更改\n\n启动成功后，我们校验下初始化的几个用户：\n\n首先是admin，你会发现用户是通过逗号分割、分组的，你要适用ldap的这种表示方法。\n\n```shell\nldapwhoami -h 127.0.0.1 -p 389 -D \"cn=admin,dc=coder4,dc=com\" -w admin123\ndn:cn=admin,dc=coder4,dc=com\n```\n\n接下来是readonly\n\n```shell\nldapwhoami -h 127.0.0.1 -p 389 -D \"cn=readonly,dc=coder4,dc=com\" -w readonly123 \ndn:cn=readonly,dc=coder4,dc=com\n```\n\n最后，我们添加两个组织结构，研发部rd和人力资源部hr：\n\n```shell\nversion: 1\n\n# rd org\ndn: ou=rd,dc=coder4,dc=com\nobjectClass: top\nobjectClass: organizationalUnit\nou: rd\n\n# hr org\ndn: ou=hr,dc=coder4,dc=com\nobjectClass: top\nobjectClass: organizationalUnit\nou: hr\n```\n\n执行添加动作：\n\n```shell\nldapadd -c -h 127.0.0.1 -p 389 -w admin123 -D \"cn=admin,dc=coder4,dc=com\" -f ./org.ldif\n```\n\n## 启用Ldap Account Manager\n\n我们通过Docker运行LAM，如下：\n\n```shell\n#!/bin/bash\n\nNAME=\"lam\"\nPUID=\"1000\"\nPGID=\"1000\"\n\ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    -e PUID=$PUID \\\n    -e PGID=$PGID \\\n    -e LDAP_DOMAIN=coder4.com \\\n    -e LDAP_SERVER=ldap://10.1.172.136:389 \\\n    -e LDAP_USER=cn=admin,dc=coder4,dc=com \\\n    -e LAM_PASSWORD=lam123 \\\n    -p 8080:80 \\\n    --detach \\\n    --restart always \\\n    ldapaccountmanager/lam:7.7\n```\n\n解释下上述配置：\n\n- 域名：与前面openldap服务的配置相关联\n\n- ldap服务器：前面ldap服务的地址\n\n- user：管理员用户名，不用输入密码\n\n- LAM密码：是部分管理功能所需要的密码，请根据需要自行修改\n\n启动成功后，我们访问http://127.0.0.1:8080，出现如下登录界面：\n\n![f](./lam-login.png)\n\n输入前面admin的密码，即可完成登录。\n\n进入后，可以发现氛围User / Group两个主要的Tab。\n\n- User：用户的增删改\n\n- Group：用户组的增删改\n\n我们首先修改下User功能默认的配置。打开右上角Tools -> Profile Editor -> User，这里设置为：\n\n- LDAP suffix：rd > coder4 > com\n\n- Automatically add this extension: false\n\n接着，我们需要添加一个Posix组，Groups -> New Group -> Unix Group\n\n- Suffix：coder4 > com\n\n- Group name：user\n\n最后，我们尝试添加一个用户，Users -> New User，在如下界面中填写：\n\n![f](./lam-create-user.png)\n\n- Last name: zhangsan\n\n- Suffix：rd > coder4 > com\n\n- RDN identifier：cn\n\n- Password：123456\n\n- Unix Primary Group：user\n\n点击Save保存后，我们验证一下：\n\n```shell\nldapwhoami -h 127.0.0.1 -p 389 -D \"cn=zhangsan,ou=rd,dc=coder4,dc=com\" -w 123456\ndn:cn=zhangsan,ou=rd,dc=coder4,dc=com\n```\n\n成功！\n\n如果你想看组织的全貌，可以进入：Tools -> TreeView：\n\n![f](./lam-tree.png)\n\n至此，我们已经成功搭建了基于ldap的内网统一验证。然而，本节只是一个起点，在后续搭建的系统中，我们都会接入ldap认证系统。\n"
  },
  {
    "path": "src/ch07-tools/microservice-template.md",
    "content": "# 微服务模板工具\n\n在微服务架构下，我们经常需要按业务领域进行拆分，新建微服务。\n\n频繁的创建新服务，十分繁琐，本文介绍一种微服务创建的模板工具。\n\n在Maven架构下，我们可以用[ArchType]([Maven &#x2013; Guide to Creating Archetypes](https://maven.apache.org/guides/mini/guide-creating-archetypes.html))快速生成新项目。\n\n但在本文所选的Gradle构建工具下，尚未有类似工具。\n\n我们使用模板替换的方式，新建服务。\n\n## 构建模板微服务\n\n首先，我们构建模板微服务，代码放到了[这里](https://github.com/liheyuan/homs-microservice-template)。\n\n我们看下目录结构：\n\n```shell\n.\n├── build.gradle\n├── gradle\n│   └── wrapper\n│       ├── gradle-wrapper.jar\n│       └── gradle-wrapper.properties\n├── gradlew\n├── gradlew.bat\n├── homs-template-client\n│   ├── build.gradle\n│   └── src\n│       └── main\n│           ├── java\n│           │   └── com\n│           │       └── coder4\n│           │           └── homs\n│           │               └── template\n│           │                   ├── HomsTemplate.proto\n│           │                   ├── HomsTemplateGrpc.java\n│           │                   ├── HomsTemplateProto.java\n│           │                   ├── client\n│           │                   │   ├── AbstractGrpcClientManager.java\n│           │                   │   ├── HSGrpcClient.java\n│           │                   │   ├── HomsAbcGrpcClient.java\n│           │                   │   └── SimpleGrpcClientManager.java\n│           │                   └── constant\n│           │                       └── HomsAbcConstant.java\n│           └── resources\n│               └── META-INF\n│                   └── spring.factories\n├── homs-template-server\n│   ├── build.gradle\n│   ├── lombok.config\n│   └── src\n│       ├── main\n│       │   ├── java\n│       │   │   └── com\n│       │   │       └── coder4\n│       │   │           └── homs\n│       │   │               └── template\n│       │   │                   └── server\n│       │   │                       └── server\n│       │   │                           ├── HomsRpcServer.java\n│       │   │                           ├── HomsTemplateApplication.java\n│       │   │                           ├── configuration\n│       │   │                           │   ├── RpcBindableServiceConfiguration.java\n│       │   │                           │   └── RpcServerConfiguration.java\n│       │   │                           ├── grpc\n│       │   │                           │   └── HomsTemplateGrpcImpl.java\n│       │   │                           └── web\n│       │   │                               ├── ctrl\n│       │   │                               │   └── BaseController.java\n│       │   │                               ├── logic\n│       │   │                               │   ├── impl\n│       │   │                               │   └── spi\n│       │   │                               └── vo\n│       │   └── resources\n│       │       └── application.yaml\n│       └── test\n│           └── java\n│               └── com\n│                   └── coder4\n│                       └── homs\n│                           ├── demo\n│                           └── template\n│                               └── server\n│                                   └── server\n│                                       └── Test.java\n├── settings.gradle\n└── tool\n    ├── compile_grpc.sh\n    └── test_curl.sh\n\n```\n\n如上图所示，这是一个多模块的子项目，分为client、server两部分。与我们在前文中介绍的保持一致。为了简单起见，这里去掉了MySQL、Redis等依赖。\n\n## 服务生成工具\n\n接下来，我们开发服务生成工具，脚本如下：\n\n```shell\n#!/bin/bash\n\nif [ x\"$#\" != x\"1\" ];then\n\techo \"Usage $0 <project-name>\"\n\texit -1\nfi\n\nPROJECT_NAME=$1\nPROJECT_NAME_CAMEL=$(echo $PROJECT_NAME | gsed -r 's/(^|-)([a-z])/\\U\\2/g')\nPROJECT_P1=$(echo $PROJECT_NAME | awk -F '-' '{print $1}')\nPROJECT_P2=$(echo $PROJECT_NAME | awk -F '-' '{print $2}')\n\nrm -rf $PROJECT_NAME\ncp -rf homs-template $PROJECT_NAME\n\n# move files\nmv $PROJECT_NAME/homs-template-client $PROJECT_NAME/${PROJECT_NAME}-client\nmkdir -p $PROJECT_NAME/${PROJECT_NAME}-client/src/main/java/com/coder4/$PROJECT_P1/$PROJECT_P2\nmv $PROJECT_NAME/${PROJECT_NAME}-client/src/main/java/com/coder4/homs/template/* $PROJECT_NAME/${PROJECT_NAME}-client/src/main/java/com/coder4/$PROJECT_P1/$PROJECT_P2\nrm -rf $PROJECT_NAME/${PROJECT_NAME}-client/src/main/java/com/coder4/homs/template\n\nmv $PROJECT_NAME/homs-template-server $PROJECT_NAME/${PROJECT_NAME}-server\nmkdir -p $PROJECT_NAME/${PROJECT_NAME}-server/src/main/java/com/coder4/$PROJECT_P1/$PROJECT_P2\nmv $PROJECT_NAME/${PROJECT_NAME}-server/src/main/java/com/coder4/homs/template/* $PROJECT_NAME/${PROJECT_NAME}-server/src/main/java/com/coder4/$PROJECT_P1/$PROJECT_P2\nrm -rf $PROJECT_NAME/${PROJECT_NAME}-server/src/main/java/com/coder4/homs/template\n\nfind $PROJECT_NAME -type file -exec gsed -i \"s/HomsTemplate/$PROJECT_NAME_CAMEL/g\" {} +\nfind $PROJECT_NAME -type file -exec gsed -i \"s/homs\\.template/$PROJECT_P1\\.$PROJECT_P2/g\" {} +\nfind $PROJECT_NAME -type file -exec gsed -i \"s/homs-template/$PROJECT_P1-$PROJECT_P2/g\" {} +\nfor file in $(find $PROJECT_NAME -type file);do\n\ttarget=$(echo $file|sed -e \"s/HomsTemplate/$PROJECT_NAME_CAMEL/g\")\n\tmv $file $target\ndone\n```\n\n如上所示：\n\n- 输入项目\"homs-abc\"后，会获取其驼峰命名如\"HomsAbc\"\n\n- 拷贝上述template项目后，会对文件夹进行重命名\n\n- 接着，对文件中的template进行替换\n\n- 最后，对部分文件名进行替换\n\n我们试着这运行下：\n\n```shell\n./generate.sh homs-abc\n\n├── build.gradle\n├── gradle\n│   └── wrapper\n│       ├── gradle-wrapper.jar\n│       └── gradle-wrapper.properties\n├── gradlew\n├── gradlew.bat\n├── homs-abc-client\n│   ├── build.gradle\n│   └── src\n│       └── main\n│           ├── java\n│           │   └── com\n│           │       └── coder4\n│           │           └── homs\n│           │               └── abc\n│           │                   ├── HomsAbc.proto\n│           │                   ├── HomsAbcGrpc.java\n│           │                   ├── HomsAbcProto.java\n│           │                   ├── client\n│           │                   │   ├── AbstractGrpcClientManager.java\n│           │                   │   ├── HSGrpcClient.java\n│           │                   │   ├── HomsAbcGrpcClient.java\n│           │                   │   └── SimpleGrpcClientManager.java\n│           │                   └── constant\n│           │                       └── HomsAbcConstant.java\n│           └── resources\n│               └── META-INF\n│                   └── spring.factories\n├── homs-abc-server\n│   ├── build.gradle\n│   ├── lombok.config\n│   └── src\n│       ├── main\n│       │   ├── java\n│       │   │   └── com\n│       │   │       └── coder4\n│       │   │           └── homs\n│       │   │               └── abc\n│       │   │                   └── server\n│       │   │                       └── server\n│       │   │                           ├── HomsAbcApplication.java\n│       │   │                           ├── HomsRpcServer.java\n│       │   │                           ├── configuration\n│       │   │                           │   ├── RpcBindableServiceConfiguration.java\n│       │   │                           │   └── RpcServerConfiguration.java\n│       │   │                           ├── grpc\n│       │   │                           │   └── HomsAbcGrpcImpl.java\n│       │   │                           └── web\n│       │   │                               ├── ctrl\n│       │   │                               │   └── BaseController.java\n│       │   │                               ├── logic\n│       │   │                               │   ├── impl\n│       │   │                               │   └── spi\n│       │   │                               └── vo\n│       │   └── resources\n│       │       └── application.yaml\n│       └── test\n│           └── java\n│               └── com\n│                   └── coder4\n│                       └── homs\n│                           ├── demo\n│                           └── template\n│                               └── server\n│                                   └── server\n│                                       └── Test.java\n├── settings.gradle\n└── tool\n    ├── compile_grpc.sh\n    └── test_curl.sh\n\n\n```\n\n如上，非常快速的生成了新的微服务！\n\n在实际项目中，你还可以在初始化脚本中，集成如下功能：\n\n- 自动创建远程的git repo\n\n- 创建jenkins打包项目\n\n- 创建监控项\n\n由于篇幅所限，这里不再讨论上述功能改进。\n\n\n\n\n\n\n"
  },
  {
    "path": "src/ch07-tools/registry2.md",
    "content": "## 使用Registry2搭建Docker私有仓库\n\n在[打造持续交付流水线](../ch06-cd/README.md)一章中，在部署前，需要先打包Docker镜像，并上传到DockerHub镜像仓库。\n\nDockerHub是由Docker推出的共有镜像仓库，使用广泛，但存在一下问题：\n\n- 由于众所周知的原因，从国内访问速度较慢\n\n- 对公网所有用户可见，存在泄密风险\n\n- 存在泄露风险\n\n因此，搭建私有的容器镜像仓库，十分必要。\n\n本节，我们将基于Docker官方的registry2，搭建私有镜像仓库。\n\n## 启动\n\n我们用Docker启动Docker镜像仓库：-）\n\n```shell\n#!/bin/bash\n\nNAME=\"registry2\"\nPUID=\"1000\"\nPGID=\"1000\"\n\nVOLUME=\"$HOME/docker_data/registry2\"\nmkdir -p $VOLUME \n\ndocker ps -q -a --filter \"name=$NAME\" | xargs -I {} docker rm -f {}\ndocker run \\\n    --hostname $NAME \\\n    --name $NAME \\\n    --volume $VOLUME:/var/lib/registry \\\n    --env REGISTRY_STORAGE_DELETE_ENABLED=true \\\n    --env PUID=$PUID \\\n    --env PGID=$PGID \\\n    -p 5000:5000 \\\n    --detach \\\n    --restart always \\\n    registry:2\n```\n\n如上所示，我们添加了允许删除镜像的配置。\n\n启动成功后，镜像仓库运行在 http://127.0.0.1:5000 地址上。\n\n由于我们未启用https证书校验，因此需要在客户端上配置：\n\n/etc/docker/daemon.json中添加一行\n\n```json\n\"insecure-registries\":[\"10.1.172.136:5000\",\"127.0.0.1:5000\"],\n```\n\n## 上传镜像\n\n打tag\n\n```shell\ndocker tag 7aa22139eca1 127.0.0.1:5000/jenkins-my-agent:latest\n```\n\n上传，成功！\n\n```shell\ndocker push 127.0.0.1:5000/jenkins-my-agent:latest\nThe push refers to repository [127.0.0.1:5000/jenkins-my-agent]\n25af0e804bd9: Pushed \nd481382bb71b: Pushed \n9a0d9a003e42: Pushed \nd90590887490: Pushed \n2e10e3c8baa6: Pushed \n260e081d58bf: Pushed \n545b9645e192: Pushed \ned0f1dee792d: Pushed \nebb837d412f9: Pushed \nb80c59a58a8e: Pushed \n953a3e11bab6: Pushed \n833c84c9f2ea: Pushed \n7a45298bdd53: Pushed \n62a747bf1719: Pushed \nlatest: digest: sha256:3b7ebd6948da5d7d9267e02b58698c3046e940f565eab9687243aaa8727ace29 size: 3266\n```\n\n我们查询下历史版本，这里发现有一个latest的版本了\n\n```shell\ncurl \"127.0.0.1:5000/v2/jenkins-my-agent/tags/list\"\n{\"name\":\"jenkins-my-agent\",\"tags\":[\"latest\"]}\n```\n\n尝试删除镜像，成功！\n\n```shell\nregistry='localhost:5000'\nname='jenkins-my-agent'\ncurl -v -sSL -X DELETE \"http://${registry}/v2/${name}/manifests/$(\n    curl -sSL -I \\\n        -H \"Accept: application/vnd.docker.distribution.manifest.v2+json\" \\\n        \"http://${registry}/v2/${name}/manifests/$(\n            curl -sSL \"http://${registry}/v2/${name}/tags/list\" | jq -r '.tags[0]'\n        )\" \\\n    | awk '$1 == \"Docker-Content-Digest:\" { print $2 }' \\\n    | tr -d $'\\r' \\\n)\"\n```\n\n至此，我们成功搭建了私有镜像。以下是拓展练习，留给你来实现：\n\n- 启用https证书(自签)\n\n- 支持每个容器保留最近5个tag\n\n- 将[打造持续交付流水线](../ch06-cd/README.md)中的镜像仓库，替换为私有仓库\n"
  },
  {
    "path": "src/ch07-tools/seafile.md",
    "content": "# 搭建Seafile共享云盘\n\n在企业内部，文件的共享，交换是十分重要的需求。\n\n本节，我们将搭建基于Seafile的共享云盘。\n\n## 安装\n\n首先，确保你的机器上已经安装了docker-compose。\n\n接着，下载最新docker-compose，见[地址]([Seafile Server](https://download.seafile.com/d/320e8adf90fa43ad8fee/files/?p=/docker/docker-compose.yml))\n\n我们需要略做修改：\n\n```yaml\nversion: '2.0'\nservices:\n  db:\n    image: mariadb:10.5\n    container_name: seafile-mysql\n    environment:\n      - MYSQL_ROOT_PASSWORD=seafile123  # Requested, set the root's password of MySQL service.\n      - MYSQL_LOG_CONSOLE=true\n    volumes:\n      - /Users/coder4/docker_data/seafile-mysql/db:/var/lib/mysql  # Requested, specifies the path to MySQL data persistent store.\n    networks:\n      - seafile-net\n\n  memcached:\n    image: memcached:1.5.6\n    container_name: seafile-memcached\n    entrypoint: memcached -m 256\n    networks:\n      - seafile-net\n\n  seafile:\n    image: seafileltd/seafile-mc:latest\n    container_name: seafile\n    ports:\n      - \"80:80\"\n      - \"443:443\"  # If https is enabled, cancel the comment.\n    volumes:\n      - /Users/coder4/docker_data/seafile-data:/shared   # Requested, specifies the path to Seafile data persistent store.\n    environment:\n      - DB_HOST=db\n      - DB_ROOT_PASSWD=seafile123  # Requested, the value shuold be root's password of MySQL service.\n      - TIME_ZONE=Etc/UTC  # Optional, default is UTC. Should be uncomment and set to your local time zone.\n      - SEAFILE_ADMIN_EMAIL=me@example.com # Specifies Seafile admin user, default is 'me@example.com'.\n      - SEAFILE_ADMIN_PASSWORD=123456     # Specifies Seafile admin password, default is 'asecret'.\n      - SEAFILE_SERVER_LETSENCRYPT=true   # Whether to use https or not.\n      - SEAFILE_SERVER_HOSTNAME=seafile.coder4.com # Specifies your host name if https is enabled.\n    depends_on:\n      - db\n      - memcached\n    networks:\n      - seafile-net\n\nnetworks:\n  seafile-net:\n```\n\n如上：\n\n- 修改了数据库的默认密码\n\n- 修改了volume的路径到本地~/docker_data下\n\n- 修改seafile的管理员密码\n\n- 并开启https和域名，要求域名必须公网可见\n\n最后，我们创建所需的本地volume目录，并重启：\n\n```shell\nmkdir /Users/coder4/docker_data/seafile-mysql/\nmkdir /Users/coder4/docker_data/seafile-data  \n```\n\n```shell\ndocker-compose up -d\n```\n\n浏览器 打开 地址 https://seafile.coder4.com 应该能出现如下登录界面了：\n\n![f](./seafile.png)\n\n## 配置ldap\n\n接下来，我们配置ldap\n\n打开文件~/docker_data/seafile-data/seafile/conf/ccnet.conf，添加如下内容：\n\n```shell\n[LDAP]\nHOST = ldap://10.1.172.136:389/\nBASE = dc=coder4,dc=com\nUSER_DN = cn=readonly,dc=coder4,dc=com\nPASSWORD = readonly123\nLOGIN_ATTR = cn \n```\n\n重启服务\n\n```shell\ndocker-compose\n```\n\n使用zhangsan / 123546登录，成功！\n\n下面，你可以尝试上传文件、共享群组了，这里不再赘述。\n"
  }
]