master 9c7da66a030d cached
128 files
556.3 KB
211.4k tokens
1 requests
Download .txt
Showing preview only (802K chars total). Download the full file or copy to clipboard to get everything.
Repository: liheyuan/hands-on-microservices
Branch: master
Commit: 9c7da66a030d
Files: 128
Total size: 556.3 KB

Directory structure:
gitextract_6y7hg2hl/

├── .gitignore
├── README.md
├── book.toml
├── legacy/
│   ├── SUMMARY.md
│   ├── architecture/
│   │   ├── README.md
│   │   ├── devops.md
│   │   ├── microservics.md
│   │   ├── ms-arch.xml
│   │   ├── overview.md
│   │   └── toolchain.md
│   ├── devops/
│   │   ├── README.md
│   │   ├── discovery.md
│   │   ├── docker-repo.md
│   │   ├── jump-server.md
│   │   └── openvpn-k8s.md
│   ├── k8s/
│   │   ├── README.md
│   │   ├── docker-k8s.md
│   │   ├── helm.md
│   │   ├── k8s-cluster.md
│   │   ├── k8s-ha.md
│   │   ├── k8s-intro.md
│   │   ├── k8s-ipvs.md
│   │   └── k8s-office.md
│   ├── ms-circuit-breaker-and-limit/
│   │   ├── README.md
│   │   ├── sb-hystrix.md
│   │   └── sb-limit.md
│   ├── ms-config/
│   │   ├── README.md
│   │   ├── cfg4j.md
│   │   ├── consul-devops.md
│   │   └── sb-config.md
│   ├── ms-delivery/
│   │   ├── README.md
│   │   ├── jenkins-devops.md
│   │   ├── ms-cd.md
│   │   └── ms-ci.md
│   ├── ms-discovery/
│   │   ├── README.md
│   │   ├── msd.md
│   │   └── service-discovery.xml
│   ├── ms-log/
│   │   ├── README.md
│   │   ├── elk-devops.md
│   │   ├── sb-eblk.md
│   │   ├── sb-logback.md
│   │   └── sb-trace.md
│   ├── ms-monitor/
│   │   ├── README.md
│   │   ├── k8s-prometheus-grafana.md
│   │   ├── sb-prometheus.md
│   │   ├── sb-sentry.md
│   │   ├── sentry-devops.md
│   │   └── sentry.txt
│   ├── ms-msgq/
│   │   ├── README.md
│   │   ├── dev-kafka.md
│   │   ├── kafka-devops.md
│   │   ├── rabbitmq-devops.md
│   │   ├── rocketmq-devops.md
│   │   ├── sb-kafka.md
│   │   ├── sb-rabitmq.md
│   │   └── sb-rocketmq.md
│   ├── ms-storage/
│   │   ├── README.md
│   │   ├── memcached-devops.md
│   │   ├── mysql-devops.md
│   │   ├── redis-devops.md
│   │   ├── sb-memcached.md
│   │   ├── sb-mysql.md
│   │   └── sb-redis.md
│   ├── spring-boot/
│   │   ├── README.md
│   │   ├── discovery.md
│   │   ├── gerrit.md
│   │   ├── graceful-shutdown.xml
│   │   ├── mockito.md
│   │   ├── rest-nginx.xml
│   │   ├── sb-gradle-structure.md
│   │   ├── sb-mockito.md
│   │   ├── sb-rest.md
│   │   └── sb-thrift.md
│   └── toolchain/
│       ├── README.md
│       ├── bom.md
│       ├── gerrit.md
│       ├── kanboard.md
│       ├── ldap.md
│       ├── nexus.md
│       ├── spring-boot-scripts.md
│       ├── spring-boot-template.md
│       └── stress-test.md
└── src/
    ├── README.md
    ├── SUMMARY.md
    ├── ch01-architecture/
    │   ├── README.md
    │   ├── continuous-x.md
    │   ├── micro-service-intro.md
    │   ├── ms-arch.plantuml
    │   ├── ms-architecture.md
    │   ├── ms-tech-stack.md
    │   └── rd-ops-toolchain.md
    ├── ch02-ms-dev1/
    │   ├── README.md
    │   ├── database1.md
    │   ├── database2.md
    │   ├── gradle.md
    │   ├── redis.md
    │   ├── rpc.md
    │   └── spring-boot.md
    ├── ch03-ms-dev2/
    │   ├── README.md
    │   ├── circuit-breaker-and-limiter.md
    │   ├── config.md
    │   ├── mq.md
    │   ├── registry1.md
    │   └── registry2.md
    ├── ch04-ms-dev3/
    │   ├── README.md
    │   ├── elkfk.md
    │   ├── micrometer.md
    │   ├── skywalking.md
    │   └── victorialmetrics.md
    ├── ch05-k8s/
    │   ├── README.md
    │   ├── container.md
    │   ├── k8s-101.md
    │   ├── k8s-cluster.md
    │   ├── k8s-ha-cluster.md
    │   └── k8s-ingress.md
    ├── ch06-cd/
    │   ├── README.md
    │   ├── jenkins-custom.md
    │   ├── jenkins-k8s-optimize.md
    │   ├── jenkins-k8s.md
    │   └── jenkins.md
    └── ch07-tools/
        ├── .md
        ├── README.md
        ├── gitlab.md
        ├── jfrog-artifactory.md
        ├── ldap.md
        ├── microservice-template.md
        ├── registry2.md
        └── seafile.md

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
book
.DS_Store


================================================
FILE: README.md
================================================
# 从0到1实战微服务架构(开源电子书)

## 前言

微服务是继SOA后,最流行的服务架构风格之一。

按照微服务对系统进行拆分后,每个服务的业务逻辑都更加简单、清晰。服务之间是松耦合的,模块之间的边界也更加清晰。

微服务有效降低了软件项目的业务复杂程度,为小团队独立开发、持续交付和部署打下了良好的基础。

遗憾的是,微服务并不是银弹。与传统的单一架构相比,微服务架构对团队的组织架构、技术水平、运维能力等方面,都提出了更高的要求。如果没有掌握得当的方法而生搬硬套,微服务架构只会会适得其反--降低项目的开发效率,这是本书的创作初衷之一。

在国内外的技术社区中,比较推崇现有开源方案,如"Spring Cloud全家桶"或者阿里开源的"Dubbo"。

上述框架通常已经实现了服务发现、配置、负载均衡、限流熔断,等微服务架构所必须的的核心功能。

使用开源框架省却了造轮子的过程,但也降低了我们学习、思考的动力。

为什么需要服务发现,又如何实现它呢?配置中心呢....思考和设计的过程充满了挑战,也是提升自身架构能力的一种手段。这是本书的创作初衷之二。

已有的微服务资料过于重视微服务的开发,忽略了微服务赖以生存的生态系统:工具链、自动化运维。可以说,离开了这两点的支持,微服务架构将难以落地。完善这两方面的思考和实战,是本书的创作初衷之三。

为此,我撰写了这本《从0到1实战微服务架构》。让我们"暂时忘掉"已有的、成熟的开源解决方案。尝试亲自动手,实现微服务架构的各个模块。

我们会从微服务开发、工具链、运维这三个角度,阐述微服务架构的实战方案。

如果本书帮助了你,欢迎在在[github](https://github.com/liheyuan/hands-on-microservices)加Star,但严禁用于商业用途!(参见本页底部版权声明)

由于能力水平所限,本书难免存在各种错误,恳请各位进行指正(Issue or PR),谢谢!

## 2.0前言

自从本书发布了1.0版本后,已过去2年多了。

技术圈又发生了很多变化,与本书密切相关的有:

* Spring Boot 2.0 稳定版发布
* Kubernetes下的包管理项目“Helm”,正式加入CNCF基金会

为此,我开启了本书2.0版的写作计划。

由于gitbook项目已不在维护,我们改用改用[mdbook](https://github.com/rust-lang/mdBook)做为图书渲染工具。

本书的写作工具为[MarkText](https://github.com/marktext/marktext)。

写作水平有限,还请各位多提宝贵意见。

## 读者基础

由于篇幅、精力所限,本书无法写成一本”零起点”教程。我假设读者具有至少2年的服务端工作经验,并且了解以下技术或原理:

* Git
* Maven & Gradle
* Docker & Kubernetes
* Java
* Spring / Spring Boot 
* 数据库: 如MySQL
* 消息队列: 如RabbitMQ
* 缓存系统: 如Memcached 
* 内存数据库: 如Redis

本书可以供架构师、项目经理、高级服务端程序员参考、学习。

动手实战是本书的核心内容,因此本书所涉及的全部代码,都托管到了我的[Github上](https://github.com/liheyuan)(以lmsia-开头的项目)。

这些代码以研讨为主要目的,也可以直接应用于生产,但本人不对其稳定性负责。

## 版权

本书虽然在github上公开写作,但版权归本人[Coder4](https://coder4.com)所有。

依照 [署名-非商业性使用-相同方式共享](https://creativecommons.org/licenses/by-nc-sa/2.5/cn/) ,任何人可以在保留署名的情况下转载。但严禁用于商业用途。

This is a book powered by [mdBook](https://github.com/rust-lang/mdBook).


================================================
FILE: book.toml
================================================
[book]
authors = ["lihy"]
language = "en"
multilingual = false
src = "src"
title = "从0到1实战微服务架构(第2版)"


================================================
FILE: legacy/SUMMARY.md
================================================
# Summary

* [从0到1实战微服务架构](README.md)

* [架构概览](architecture/README.md)
    * [微服务架构概览](architecture/overview.md)
    * [运维技术链概览](architecture/devops.md)
    * [微服务技术栈概览](architecture/microservics.md)
    * [研发工具链概览](architecture/toolchain.md)
    
* [Kubernetes快速入门](k8s/README.md)
  * [集装箱、容器化、容器编排](k8s/docker-k8s.md)
    * [Kubernetes 快速入门](k8s/k8s-intro.md)
    * [搭建Kubernetes集群](k8s/k8s-cluster.md)
    * [为Kubernetes集群开启ipvs](k8s/k8s-ipvs.md)
    * [使用Helm进行包管理](k8s/helm.md)
    * [办公网与Kubernetes集群的打通](k8s/k8s-office.md)
    * [Kubernetes集群的高可用方案](k8s/k8s-ha.md)
  
* [微服务的自动发现与负载均衡](ms-discovery/README.md)

    * [微服务的自动发现与负载均衡](ms-discovery/msd.md)

    * 使用边车模式于微服务部署

* [微服务的开发框架](spring-boot/README.md)
  
    * [Gradle子项目划分与微服务的代码结构](spring-boot/sb-gradle-structure.md)
    * [Spring Boot整合Thrift RPC](spring-boot/sb-thrift.md)
    * [Spring Boot整合REST服务](spring-boot/sb-rest.md)
    * [Mockito 单元测试打桩神器](spring-boot/sb-mockito.md)
    
* [微服务的存储与缓存](ms-storage/README.md)
    * [MySQL 数据库的运维](ms-storage/mysql-devops.md)
    * [Spring Boot整合MySQL](ms-storage/sb-mysql.md)
    * [Memcached 缓存服务的运维](ms-storage/memcached-devops.md)
    * [Spring Boot整合Memcached](ms-storage/sb-memcached.md)
    * [Redis 内存数据库的运维](ms-storage/redis-devops.md)
    * [Spring Boot整合Redis](ms-storage/sb-redis.md)
    
* [微服务的消息队列](ms-msgq/README.md)
    * [RabbitMQ 消息队列的运维](ms-msgq/rabbitmq-devops.md)
    * [Spring Boot整合RabbitMQ](ms-msgq/sb-rabitmq.md)
    * [RocketMQ 消息队列的运维](ms-msgq/rocketmq-devops.md)
    * [Spring Boot整合RocketMQ](ms-msgq/sb-rocketmq.md)
    * [Kafka 流处理平台的运维](ms-msgq/kafka-devops.md)
    * [Kafka 流处理开发简介](ms-msgq/dev-kafka.md)
    
* [微服务日志监控](ms-log/README.md)
    * [Spring Boot配置Logback日志](ms-log/sb-logback.md)
    * [Spring Boot整合分布式追踪](ms-log/sb-trace.md)
    * [ELK日志分析平台的运维](ms-log/elk-devops.md)
    * [Spring Boot整合EBLK日志分析平台](ms-log/sb-eblk.md)
    
* [微服务平台监控](ms-monitor/README.md)
    * [Sentry 错误预警系统的运维](ms-monitor/sentry-devops.md)
    * [Spring Boot整合Sentry](ms-monitor/sb-sentry.md)
    * [Kubernetes + Prometheus + Grafana平台监控](ms-monitor/k8s-prometheus-grafana.md)
    
* [微服务配置中心](ms-config/README.md)
    * [cfg4j及方案简介](ms-config/cfg4j.md)
    * [Spring Boot整合配置中心](ms-config/sb-config.md)
    
* [微服务熔断与限流](ms-circuit-breaker-and-limit/README.md)
    * [熔断与Hystrix](ms-circuit-breaker-and-limit/sb-hystrix.md)
    * [限流的实现](ms-circuit-breaker-and-limit/sb-limit.md)
    
* [微服务持续交付](ms-delivery/README.md)
    * [Jenkins平台的运维](ms-delivery/jenkins-devops.md)
    * [Jenkins持续集成](ms-delivery/ms-ci.md)
    * [Jenkins持续部署](ms-delivery/ms-cd.md)
    
* [研发工具链](toolchain/README.md)
    * [LDAP 内部账号管理系统](toolchain/ldap.md)
    * [gerrit 代码的版本管理与评审](toolchain/gerrit.md)
    * [Nexus 私有maven仓库](toolchain/nexus.md)
    * [BOM 减少版本冲突](toolchain/bom.md)
    * [Spring Boot 项目模板](toolchain/spring-boot-template.md)
    * [开发效率脚本](toolchain/spring-boot-scripts.md)
    * [打压工具](toolchain/stress-test.md)
    
* [运维工具链](devops/README.md)
    * [Docker 私有仓库](devops/docker-repo.md)
    * [Nginx REST 网关自动配置](devops/discovery.md)
    * 
    * [OpenVPN访问Kubernetes集群内网](devops/openvpn-k8s.md)
    * [线上跳板机](devops/jump-server.md)



================================================
FILE: legacy/architecture/README.md
================================================
# 架构概览

当我们谈微服务架构时,我们需要关注哪些点,本章将从这里说起。在了解了微服务的整体架构后,我们会从“研发工具链”、“微服务技术栈”、“运维技术链条”三个角度展开。我们会讨论微服务架构中,如何对这三类问题进行技术选型以及做出这些选型决策的原因。同时,我们将探讨如何将这三部分有机地融入到微服务架构中。



================================================
FILE: legacy/architecture/devops.md
================================================
# 运维工具链概览

在看过微服务整体架构后,我们来讨论下架构的各个层次中,本书所选用的技术栈。

与前面类似,我们依然自底向上讨论。

运维工具链选型
* 基础设施层:对于绝大多数的中小公司,且无强烈的数据保密需求,我强烈建议使用云主机。
 * 运维成本更低。想对机器加64GB的内存,如果自运维的话跑去机房、断电、拆机器、插内存,半天过去了。如果采用云主机,就是点点鼠标,重启一下的事情。
 * 弹性更大。老板一拍脑袋,明天要上线大促,预估要扩容10倍。只有一天时间,是不可能完成100台机器的采购、上架、配置的。大促结束后,流量回归到平常情况,这100台机器还要继续空转烧钱么?如果采用云主机,这些就是点点鼠标,甚至是几个API调用就可以搞定的事情。
 * 平均稳定性更高。内存ECC故障、硬盘坏道、RAID卡故障,这些都是自运维服务器时代的家常便饭。若采用云主机,就可以很好的避免这些烦恼。常见的大厂云主机,都提供了至少3个9的在线时间保障,与运维服务器相比,平均稳定性更高。
* 运维平台层之容器管理:前面已经提到,微服务需要借助容器技术才能“顺利启航”。目前主流的方案有:docker加脚本、swarm集群、Kubernetes集群、Marathon集群。本书选用Kubernetes集群,它内置的功能完全可以满足”容器的监控、调度、管理“。在这里我们采用排除法,说说为什么不选用其他几个方案。
 * docker加脚本:2014年Docker刚刚兴起时,还没有集群管理的解决方案,多数公司采用了这类架构,在不同物理机器上,通过自动化脚本来启动不同的容器。这种方式简单直接,但是当集群规模扩大后,运维非常困难。此外,这种方式很难解决自动网络配置、自动调度、容器数据迁移等很现实的问题。
 * swarm集群:swarm集群方案是Docker公司于2014年末推出的容器集群技术方案。尽管swarm是Docker公司的“亲儿子”又是市场的先发产品,但swarm很快被后起之秀Kubernetes超越。时至今日,从的新功能跟进、社区活跃、文档完善程度等方面,都弱于Kubernetes。更重要的是,借助CNCF基金会的影响力,Kubernetes已经成为了事实上的容器集群解决方案,GCE、AWS等云平台,都原生支持Kubernetes集群。在2017年,Docker公司在自己的商业化产品中直接集成了Kubernetes,标志着swarm项目的全面落败。所以在本书中,我们也不会选用swarm集群。
 * Marathon集群:Marathon是构建在Mesos集群上的一套容器集群管理软件。想使用Marathon,先要部署一套底层的Mesos。对于大型公司,特别是已经部署了Mesos的Hadoop集群的公司来说,这种搞法可以提升机器复用程度。但对于中小规模的企业而言,Mesos + Marathon多少有一些“”高射炮打蚊子“的感觉。此外,虽然Marathon在大规模机器管理上有比较多的经验,但并未在容器编排上投入太多新功能,更像是借助容器的概念为自己"造势"。综上所述,我们也不会选用Marathon集群。
* 运维平台之持续部署系统:部署前需要先构建,本书的微服务开发选用Spring Boot框架,在构建方面,我们使用Gradle(之后会阐述原因)。在持续部署方面,我们选用老牌工具Jenkins,通过一些插件和配置完成全套的持续部署。
* 运维平台之部署版本管理系统:前面已经提到,我们将采用容器技术。本书采用自建私有Docker仓库的方式,完成容器的镜像工作,并使用它作为部署版本的管理系统。

本书的主线是微服务的架构及开发。为了保证这一主题的的稳定和连惯性,我们将上述运维工具链的使用单独抽提出来,在[《运维工具链》](../devops/README.md)一章中介绍上述内容。



================================================
FILE: legacy/architecture/microservics.md
================================================
# 微服务技术栈概览

下面来看一下微服务相关的技术选型

* 服务开发框架:在微服务开发方面,我们选用Java作为开发语言。市面上的语言众多,特别近几年来,Go、Rust语言作为服务端语言快速崛起。既然如此,我们为什么还要选用基于Java语言呢?尽管Go等语言崛起的很快,但相对依然小众,能够熟练运用这些语言进行生产开发的人才更是少之又少,从人才供给量上就无法满足业务需求。此外,新兴语言的社区相对活跃,版本变化较为频繁,经常出现各种不兼容升级,各种坑,这些都不利于业务的开发。反观Java,虽然大家都批评多年来一直是”八股文“,但一直保持向下兼容,且依然稳重求进。最终要的是,Java拥有”海量“的开发人员基础,人才供给不会成为瓶颈。因此,我们选用Java作为开发语言,并使用Spring Boot作为开发框架。Spring Boot作为Spring框架的一次"革命",可以说是为了微服务而生的,在本书的后续章节,大家会逐渐体会到选用Spring Boot的原因。
* 服务注册与发现:为了简化实现难度,我们将借助Kubernetes内置的服务和虚拟端口功能,来实现服务的注册与发现。换句话说,我们将服务注册与发现的能力,下推一层到运维平台层。对于微服务这一层,服务的注册与发现是完全透明的。
* 熔断与限流:微服务之间的调用链更加复杂,为了降低&隔离服务故障造成的影响,一般选用和熔断或限流策略。我们选用Hystrix作为熔断功能的基础库并进行了封装,Guava的RateLimit作为限流功能的基础库并进行了封装。
* 配置中心:这里主要关注配置的自动下发、自动更新和易维护性。我们采用git + cfg4j的模式实现配置中心。
* 后端组件
 * RPC:目前主流的开源RPC框架是gRPC和Thrift。gRPC在设计理念上更为先进,且原生支持HTTP2,可以直接集成到客户端上。Thrift作为老牌RPC框架,历经多家公司的考验,已经非常成熟和稳定,且性能比gRPC更为出色。因此,我们选用Thrift作为本书的RPC框架。
 * 关系型数据库:我们选用市场占有率最高的开源数据库MySQL。Spring Boot内置了多种数据库接入方式,我们会采用JPA和“裸写”DAO两种接入方法,以满足不同应用场景。
 * 内存数据库:”让系统更快“是我们不断追求的目标。当基于磁盘的MySQL数据库不能满足性能需求时,我们选用Redis内存数据库。Redis是一款高性能的内存数据库,在官方的性能评测中,其QPS可达到十万级别[^1]。在接入组件方面,我们选用Redisson。与老牌的jedis等开源库相比相比,Redisson的优势在于将Redis操作与数据结构进行了有机的结合,可以用类似Java内置数据类型的操作方式轻松地使用Redis。
 * 缓存:构建高性能的分布式系统,缓存是必不可少的。Memcached是经典的高性能分布式内存缓存系统,我们选用它作为后端缓存组件。只有后端组件是不够的,还需要与Spring Boot集成。常见的Java客户端有Spymemcached和xMemcached。由于xMemcached采用了NIO模型,我们选用它作为接入库。
 * 消息队列:当系统的同步阻塞处理频繁出现性能瓶颈,甚至拖垮整个系统时,我们可以引入消息队列,将同步处理转为异步处理。消息队列就是为这种场景而设计的。目前比较主流的开源消息队列有Kafka、RabbitMQ等。从设计理念来看,Kafka是一个成熟的分布式流处理平台,更专注于海量消息和分布式拓展性。RabbitMQ则更加专注于消息队列,且兼容AMQP协议。结合我们的需求,选用RabbitMQ更为合适。虽然Spring内置了AMQP的集成方案,但使用起来略为繁琐。我们会以官方客户端为基础,自行构建一套工具类库。
* 日志:我们选用Spring Boot默认的logback作为日志记录系统;使用"EBLK"组合作为日志收集、分析系统。
* 监控:
 * 微服务监控:在微服务部署之后,我们需要对微服务和其所在的容器进行的健康状况进行监控。包括容器的内存、CPU、网络状况,以及微服务的GC等信息。我们选用Prometheus作为数据的收集和查询系统,Grafana作为监控可视化平台。我们会探讨如何向Prometheus发送自定义的监控数据。
 * 业务异常报警:微服务监控可以帮助我们了解服务的健康情况,定位性能瓶颈。但对于系统的业务异常却无能为力。Sentry是一款错误追踪系统,可以帮助我们发现并定位逻辑异常。类似的,我们也会探讨如何集成Spring Boot与Sentry。

[^1]: [How fast is Redis?] (https://redis.io/topics/benchmarks)


================================================
FILE: legacy/architecture/ms-arch.xml
================================================
<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>

================================================
FILE: legacy/architecture/overview.md
================================================
# 微服务架构概览

在正式讨论微服务架构前,有必要用简短的篇幅,讨论下微服务以及这种架构风格的优点和缺点。

在微服务出现之前,我们的架构多数是单体应用架构。会有一个或者少数几个”巨无霸“进程,里面可能包含了”用户管理“、”订单管理“、”支付确认“、”物流“等等各种复杂的业务逻辑和功能。这种传统的单块应用架构风格是很直观、自然的,然而在现代软件开发领域,特别是互联网开发领域中,单块架构遇到了一些问题。

单块架构的缺点

* 耦合严重:单块服务内的各个逻辑之间,往往缺乏清晰的边界。导致内部耦合严重,正所谓“牵一发而动全身”。
* 维护困难:单快服务包含了过多的业务,代码量严重膨胀。开发人员难免”失焦“,不知道如何下手。
* 团队协作困难:如果多人同时开发同一个单块应用,势必导致代码冲突成为常态,团队协作成本急剧上升。
* 测试困难:单块服务是作为一个整体进行开发、上线的。尽管你只对A功能进行了修改,但难免会影响B功能。随着单块应用的愈发膨胀,测试工作量会提升数倍。

上述单块应用的缺点,在传统软件开发中,尚可通过“小心规划”、“人海战术”等方法解决。但到了互联网时代,就很难实现了。为什么这么讲呢?互联网软件开发,讲究的是“快糙猛”,对迭代速度的要求非常高。对于成熟的互联网企业,一个项目在一天内上线好几次,都是稀松平常的事情。试想一下我们的单块“巨无霸”服务,稍微改动一点,就要经过复杂的代码评审、测试验证,如何才能跟上快节奏的迭代和上线呢?

尽管微服务不是银弹,但微服务的出现,确实从一定程度上改善了这种情况。微服务是⼀种架构⻛格,将单块应⽤划分成⼀组⼩的服务,服务之间相互协作,以组合的形式,实现复杂的业务功能。

微服务架构的优点

* 低耦合:在单块服务中,不同业务的逻辑耦合在一起。做微服务拆分后,微服务内只包含有限的业务逻辑,耦合也随之大大降低。
* 易维护:微服务内部只包含单一业务逻辑,功能更为集中,更容易开发人员聚焦问题和修改。
* 适合团队协作:拆分为微服务后,每个服务中涉及的代码和功能更少,可以将不同微服务划分给不同团队甚至个人负责。各司其职后,有效降低了开发和代码冲突,使得其适合团队协作。
* 测试成本低:在单块服务中,哪怕只改动了一点点代码,也需要对整个巨无霸服务进行测试。微服务拆分后,功能的修改,只需要涉及改动的个别微服务进行测试,有效降低了测试工作量。
* 易横向拓展:在单块服务时代,巨无霸服务已经占用了大量资源,机器的配置已经很高,若要再单独部署一个结点做负载均衡,成本会非常很高,所以多数情况下,都是通过[纵向拓展](http://blog.51cto.com/linuxgp/764622)的方式提升系统性能(如加内存,换个更好的cpu)。采用微服务拆分后,各个微服务占用的资源更少,可以轻松的通过增加节点的横向拓展方式,提升系统性能。更进一步的说,根据[阿姆达尔定律](https://zh.wikipedia.org/zh-hans/阿姆达尔定律),微服务拆分后,各个微服务对性能的要求并不一致,可以优先拓展那些具有性能短板的微服务,有效降低了拓展成本。
* 技术选择更多样:由于微服务是各自独立的进程。各个团队可以根据自己的需要选择不同的技术方案。然而在实践中,我并不推荐这么搞,这会在后面技术选型时展开。
* 加快迭代速度:上面已经提到,微服务低耦合、易维护、适合团队协作、测试起来成本更低,也更易于横向拓展。采用微服务架构后,可以显著的提升迭代速度。

尽管微服务具有这些优点,我想再次强调:“”微服务不是银弹“”,他解决了单一服务的软件复杂度,提升了迭代速度,但也带来了一些缺点。

微服务架构的缺点

* 运维难度加大:在单块架构中,不管改动多少需求,只需要上线一个服务。而采用微服务后,为了一个需求,可能要上线一堆服务。
* 开发能力要求高:在单块架构中,巨无霸服务的逻辑都在一个项目中。采用微服务后,逻辑分散在不同的项目中,在加上微服务架构本身引入的新技术,对开发能力提出了更高挑战。
* 调试难度更大:在单块架构中,我们只需要关注一个或少数几个服务。应用微服务架构后,后端系统演变为分布式系统,可能会出现A调用B,B调用C,甚至C还要调用D的长调用链,势必增加了调试和问题排查的难度。

幸运的是,通过容器等的新技术的引入,以及合理的架构设计,这些缺点都可以得到一定程度的缓解。本书会在后续章节对这些问题进行展开。

在讨论了微服务的优缺点之后,我们可以看一下微服务的整体架构了。

![微服务整体架构](./ms-arch.png "微服务整体架构")

如上图所示,微服务架构可以大致分为五个层次,我们自底向上,逐层做一下解释。

* 基础设施层
  * 微服务是后端服务,最终一定要部署在基础设施的某台机器上。基础设施层可以是自运维的服务器或机架,也可以选用云计算的虚拟机。
  * 在这一层,我们重点关注底层“物理资源”[^1]的可用性及调整:计算资源(CPU 和 GPU)、存储资源(内存、硬盘)、网络资源(交换机、路由)。对这些基础设施的维护,或者是直接在机房维护,或者是操作云平台的API。
  * 举几个例子:大促前预估系统容量不足,我们要加半个机架的机器;直播平台今晚要来大V,预计带宽不足,要增加带宽。这些都是在基础设施层要关注的内容。
* 运维平台层
  * 对于后端服务的运维工作,持续交付是最重要的能力。由于微服务数量众多,一般需要构建一个持续交付系统,来完成微服务的自动运维,如微服务的初始化、发布、回滚、扩容。
  * 采用微服务架构后,上线的次数、频率都会显著提升,这就需要一个上线的镜像版本管理系统,记录上线的镜像版本。在微服务架构中,一般采用容器技术来实现微服务的自动运维。
  * 容器是轻量级资源,隔离能力相对较弱,我们需要对容器资源进行监控,调度和管理。举个例子:我们有三台物理机,现在物理机A和C各运行了20个容器,物理机B运行了22个容器,假设每个容器的资源占用完全一致,那么资源调度系统会自动地,将B的两个的容器调整到物理机A和C上。
* 微服务设施层
  * 假设服务A需要调用服务B,那么A服务如何获取服务B的地址(例如IP和端口)呢?在传统的单块架构中,服务数量很少,尚可采用同在配置中写死IP、端口的方法来解决这个问题。但在微服务时代,面对动辄成百上千的微服务,这种做法将不再可行。因此,如何自动注册、发现微服务的多个实例,是架构必须解决的核心问题。
  * 微服务拆分后,我们的系统从单机演化为分布式系统,为了防止分布式系统的雪崩效应[^2]、微服务设施需要有的熔断和限流的能力。
  * 面对众多的微服务,逐一修改配置的方式显得更加笨拙,我们需要有一个中央配置平台,快速完成微服务的配置修改。
  * 前面已经提到,微服务的分布式架构,会加大调试难度,所以日志的收集和预警显得更加重要,同时,我们可以根据业务日志来进行一些监控预警,这和平台层的容器监控预警是不一样的。
  * 微服务需要集成一些后端组件,如数据库、消息队列、缓存等。
* 业务服务层:在提供了微服务的基础设施后,我们可以放手开发各个微服务了。业务服务层是一些“基础微服务”或“业务微服务”,他们“各司其职”,服务之间的耦合应当做到最低。
* 接入网关层:微服务面向的“用户”,一般是Web、移动端、PC端等。出于用户体验的考量,服务端提供给客户端的的接口应当尽量实现结果聚合,减少请求次数。因此,一般要设置一层接入网关,他负责调用业务微服务A B C等,完成结果聚合后,一并返回给客户端。

在绝大多数的公司中,业务规模不会很大,所需要的机器也非常有限,基础设施层往往会使用云平台或机房托管的外包方式完成。因此,本书将不对基础设施层做过多讨论。

[^1]: 由于云主机的虚拟化技术对我们的架构透明,我们也将其看作一种物理资源。
[^2]: [分布式应用雪崩效用](http://youyu4.iteye.com/blog/2405976)


================================================
FILE: legacy/architecture/toolchain.md
================================================
# 研发工具链概览

子曰:“工欲善其事,必先利其器”。前面已经提到,微服务架构的对开发水平提出了更高的=要求,我们更应该注重研发工具链的建设,以提高开发效率。

* 内部帐号管理:不论大小企业,无论企业性质,都需要一个集中式的帐号管理系统,员工只需要设置一次帐号密码,就可以方便地使用各个不同的系统。我们选用了经典的OpenLDAP 作为帐号管理服务器。OA、代码服务器、Jenkins等系统,都很方便地接入LDAP,实现“一套帐号,各系统共享”。
* 代码版本管理:从架构和团队协作模式考虑,在微服务架构下,git比svn更合适作为版本管理系统[^1]。GitLab和Gerrit都是经典的Git代码托管系统。GitLab类似于GitHub适合GitFlow的分支独立开发,Gerrit侧重于代码评审。考虑到代码评审的需求较为强烈,我们选用Gerrit。
* 依赖管理:无论什么开发语言,只要引入了开源库,就需要面对依赖管理的的问题。正如Python的Pip、Ruby的Gem、Node的npm,Java中使用Maven来管理库依赖。对于企业级开发,一般采用自搭建Maven私有仓库的方式,方便内部包的部署和依赖。我们选用Nexus搭建私有仓库,它被官方Maven仓库所采用,是Maven仓库的事实标准。
* 自动构建工具:既然使用了Maven的依赖管理,那么配套工具按理也应但选用Maven。然而在微服务的开发中,版本依赖比传统系统更为复杂,Maven的xml文件会变得非常难以维护。Gradle在兼容Maven依赖管理的基础上,使用了更为简洁的DSL描述语言,且构建速度更快,插件更为丰富。因此,我们选用Gradle 4.X作为微服务的构建工具。
* 效率脚本与工具:开篇介绍微服务优缺点时已经提到。微服务架构下,经常需要新增微服务。为了降低新增成本,我们一套代码层面的脚本或工具来提升效率。我们会介绍“微服务初始化模板”、“更新Thrift RPC接口”等工具,以提升微服务的开发效率。

本书的主线是微服务的架构及开发。为了保证这一主题的的稳定和连惯性,我们将上述研发工具链的使用单独抽提出来,在[《研发工具链》](../toolchain/README.md)一章中一并介绍。

[^1]: [Why is Git better than Subversion?](https://stackoverflow.com/questions/871/why-is-git-better-than-subversion)


================================================
FILE: legacy/devops/README.md
================================================
# 运维工具链

如果你一直从事研发职位,或很少接触运维岗位,可能会觉得将"运维"放到架构设计中,有一些多余。

事实上,上述想法大错特错,我们来看几个案例:
1. "刚才的上线,研发人员是从test分支打的jar包,引入了新的bug,赶快从主分支打个jar包,我要重新上线..."
1. "在我的本地跑的很好的啊,一上到线上就NPE,这线上环境有毒!"
1. "从一大早就开始上线,每次都要重新打jar包,拷贝、重启...郁闷的是有个顽固的bug,研发改了3次,我也上了3次,现在已经晚上9点了,还没上完..."
1. "这个服务周日就挂掉了,直到周一早上才被发现,客服电话都被打爆了!"

如果你曾在一些不重视运维技术的公司呆过,一定对上述场景颇有感触。

如果你恰好没有经历过上述场景,我推荐你读一下《凤凰项目:一个IT运维的传奇故事》。

相信读完后,你会对"运维部门"有更加深刻的理解。

运维并不是简单的"将代码推上线",他至少应当包含两个职能:
1. 尝试通过"自动化"、"可重用"、"稳定"的方式解决部署问题。即打造所谓的"持续部署"平台。
1. 维护公司内的基础设施,例如后端服务所需要的数据库、消息队列等组建。
1. 监控系统运行状况,尝试自动恢复故障,对潜在的故障作出预警。

看了上述介绍后,你还会觉得在"运维"是"架构设计"中可有可无的一部分么?

当然,本书并不是以运维作为核心主题的。因此,在本章中,我们将介绍运维工作与微服务架构关系最紧密的几个部分:
1. 后端组建的运维,包括Docker仓库、数据库、缓存、消息队列
1. 日志与监控,日志的收集、异常报警系统,平台监控系统。
1. 生产、测试环境的构建,如跳板机、机房打通等。

好了,让我们开启微服务架构下,运维工作的新篇章吧。


================================================
FILE: legacy/devops/discovery.md
================================================
# Nginx REST网关自动配置



================================================
FILE: legacy/devops/docker-repo.md
================================================
# Docker 私有仓库

在前面的章节中,我们使用了Kubernetes和容器技术实现了微服务的发现、负载均衡、持续部署等需求。

然而,我们并未提到Docker镜像的配置。默认的,我们使用了Docker官方默认的Docker镜像。

然而在实际工作中,我们最好使用Docker私有仓库。

想象一下,持续部署流程中,我们会将微服务的jar包自动构建,并打成Docker镜像,推送到Docker镜像服务器,然后部署到Kubernetes集群上。

想象下,如果我们使用默认的公共镜像,等于将自己的产品完全"开源"地暴露给了互联网。这里"开源"打了引号,虽然打成jar包后都是class文件,但是可以通过反编译工具轻松的解析到源代码,和开源是差不多的。

因此,与之前的[私有maven仓库](toolchain/nexus.md)类似,我们也需要一个私有的Docker仓库。

## 启动私有仓库的Kubernetes服务

有意思的是,Docker私有仓库(Docker registry)本身也是一个Docker镜像。有没有鸡生蛋,蛋生鸡的感觉:-)

与之前所有的服务类似,我们也将Docker私有仓库部署在Kubernetes上。

首先,还是先创建物理机上的挂载点:

```shell
sudo mkdir /data/registry/

sudo chmod -R 777 /data/registry/
```

然后创建部署, lmsia-docker-registry.yaml:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: lmsia-docker-registry-deployment
spec:
  selector:
    matchLabels:
      app: lmsia-docker-registry
  replicas: 1
  template:
    metadata:
      labels:
        app: lmsia-docker-registry
    spec:
      restartPolicy: Always
      nodeSelector:
        kubernetes.io/hostname: minikube
      containers:
      - name: lmsia-docker-registry-ct
        image: registry:2.6.2
        ports:
        - containerPort: 5000
          hostPort: 5000
        volumeMounts:
        - mountPath: "/auth"
          name: volume
          subPath: auth
        - mountPath: "/var/lib/registry"
          name: volume
          subPath: registry
        env:
        - name: "REGISTRY_STORAGE_DELETE_ENABLED"
          value: "true"
      volumes:
      - name: volume
        hostPath:
          path: /data/registry/
```

在上面的描述文件中,进行了如下配置:
* 创建了registry容器2.6.2版本,暴露端口5000
* 强制绑定到物理机"minikube"上,挂掉自动重启
* 支持删除镜像

应用一下,成功:
```shell
kubectl apply -f ./lmsia-docker-registry.yaml
```

## 向私有仓库发布镜像 

在本书架构的应用场景下,私有仓库的使用场景是:
* jenkins完成自动构建,并向私有仓库发布镜像
* 其他Kubernetes节点,从私有仓库拉取镜像,启动Pod

我们这里先完成第一步,我们登录minikube来模拟发布镜像:

首先登录私有仓库
```shell
minikube ssh

$docker login localhost:5000

test
pass

```
需要说明的是,我们创建的私有仓库,默认有一个用户test/pass,如果你认为安全性不够的话,可以参考[官方文档](https://docs.docker.com/registry/deploying)自行修改,这里不再赘述。

还需要登录共有仓库
```shell
$docker login

coder4
xxxxxx
```
注意:这里共有仓库的登录步骤不可少,因为我们接下来需要在本地读取共有仓库的镜像。

然后我们编辑一个镜像Dockerfile:
```shell
FROM alpine
CMD sleep 3600
```

编译并发布到私有仓库上
```shell
$docker build -t alpine_test .
$docker tag alpine_test $DR_DOMAIN/alpine_test:test_1.0
$docker push $DR_DOMAIN/alpine_test
```

至此,我们已经发布到了私有仓库上,查询后,发现成功了:
```shell
$ curl http://localhost:5000/v2/_catalog

{"repositories":["alpine_test"]}
```

## Kubernetes从私有仓库拉取镜像

对于Kubernetes集群而言,我们不太可能登录到每台机器上手工执行docker login。

幸运的是,Kubernetes为我们提供了解决方案。

创建一个regcred,相当于一个在集群内部通用的凭证:
```shell
kubectl create secret docker-registry regcred --docker-server=192.168.99.100:5000 --docker-username=user --docker-password=pass --docker-email=lihy@coder4.com

secret "regcred" created
```

查看一下:
```shell
kubectl get secret regcred --output="jsonpath={.data.\.dockerconfigjson}" | base64 -d

{"auths":{"192.168.99.100:5000":{"username":"user","password":"pass","email":"lihy@coder4.com","auth":"dXNlcjpwYXNz"}}}
```

下面,我们来创建一个使用这个私有仓库的Pod,看一下yaml
```yaml
apiVersion: v1
kind: Pod
metadata:
  name: lmsia-private-test
spec:
  containers:
  - name: lmsia-private-test
    image: 192.168.99.100:5000/alpine_test:test_1.0
  imagePullSecrets:
  - name: regcred

```

如上,特殊的配置有:
* 我们在image定义之前,增加了私服的前缀
* 最后增加了刚才配置好的imagePullSecrets

apply之后,可以发现启动成功了。
```shell
kubectl get pods
NAME                                                READY     STATUS    RESTARTS   AGE
lmsia-docker-registry-deployment-569fd8b594-ldch2   1/1       Running   0          2m
lmsia-private-test                                  1/1       Running   0          57s
```

提醒一下,如果启动失败,并且错误原因是:
```shell
  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
```

那么,请参考这篇文章进行解决[insecure repository in minikube](https://github.com/kubernetes/minikube/blob/master/docs/insecure_registry.md)

至此,我们完成了Docker私有仓库的搭建和访问。

## 思考与拓展
* 在Docker Registry 2后,默认强制采用加密认证方式,请结合[这篇文章](http://tech.paulcz.net/2016/01/deploying-a-secure-docker-registry/),将私有仓库的部署改为加密方式。


================================================
FILE: legacy/devops/jump-server.md
================================================
# 线上跳板机



================================================
FILE: legacy/devops/openvpn-k8s.md
================================================
# OpenVPN访问Kubernetes集群内网

搭建好的Kubernetes集群中,默认是存在网络隔离的,即集群内部使用一套独立的网络,与物理网络相互隔离。

为了将内部服务暴露给外部调用,Kubernetes提供了ClusterIP等多种方式。

但在有的时候,我们能够从本地网络直接访问Kubernetes集群内网的所有结点:
* 本地调试微服务时,可能要调用许多微服务,而这些微服务所部署的Pod或Service并没有配置对外暴露的Cluster IP
* 我们需要登录Pod查看内存、日志等信息

实现这类功能,可以有两种方式:
* 在本地网络和集群内网之间,配置路由协议,从而实现互联互通。
* 通过VPN的方式,打通物理网络和集群内网。

路由配置的方式看起来最直观,但有时难以实施:
* 配置路由协议需要专业级的路由器、也需要学习路由的配置,设备采购成本、学习成本较大
* 如果本地和机房之间需要通过互联网而不是局域网访问,那么路由协议的配置需要运营商配合,基本无法实现

在本小节,我们主要介绍第二种方式,即通过VPN穿透的方式,从本地网络直接访问Kuberntes集群内网。

在深入技术细节前,我们先来了解下VPN的原理,如下图所示:

![VPN原理](./vpn.gif "VPN原理")

如上图所示,VPN通过互联网链接,建立其一条本地网络到远程内网之间的隧道。远程内网的含义是:有一台具有公网IP的网关,但内网本身没有暴露给公网。当VPN连接建立后,就可以直接从本地网络访问远程内网中的所有资源。

在本小节,我们选用OpenVPN,这是一个开源的VPN实现,选用理由有:
* 客户端支持广泛:Windows, Linux, Mac, Android,iOS等几乎所有主流平台
* 传输层协议同时支持TCP和UDP
* 协议流行,在一些软路由(如DD-WRT)上都有直接实现,方便了日后拓展

## OpenVPN路由服务的配置

配置OpenVPN路由主要分为两大步骤:
* 配置服务端
* 客户端连接

在配置OpenVPN服务前,我们再回顾一下这节的标题"OpenVPN访问Kubernetes集群内网",我们要做的是访问Kubernetes集群内网。

我们假设你已经架设了Kubernetes集群,并使用了calico网络模型。

我们先来看一下服务端的配置,在启动服务前,先要生成Open VPN所需要的密钥:

```shell
#!/bin/bash
VOLUME="$HOME/openvpn"
vpn_ip="vpn.coder4.com"
# init for first time only
rm -rf $VOLUME
mkdir -p $VOLUME 
docker run -v $VOLUME:/etc/openvpn --rm kylemanna/openvpn ovpn_genconfig -u udp://$vpn_ip -s 10.4.0.0/24
docker run -v $VOLUME:/etc/openvpn --rm -it kylemanna/openvpn ovpn_initpki 
```

如上所示,我们直接使用了封装好的[kylemanna/openvpn](https://github.com/kylemanna/docker-openvpn)这个Docker镜像。
* 上述请在集群的任意一台物理机上执行,这台物理机之后会作为OpenVPN的接入点,所以一定要有公网IP
* 生成配置到本地HOME目录的openvpn文件夹
* vpn内网范围是10.4.0.0/24,这只得是VPN隧道所使用的网段,一定要注意,不要和已有物理网络、Kubernetes网络冲突。
* 通信协议是UDP,我们假设这台物理机有DNS指向vpn.coder4.com

生成配置后,就可以启动VPN服务器了:

```shell
#!/bin/bash
 
# submit to tool node
NAME="openvpn"
VOLUME="$HOME/openvpn"
dns_ip="10.96.0.10"
 
# stop & run server (should call init_open_vpn_test.sh before) 
docker ps -q -a --filter "name=$NAME" | xargs -I {} docker rm -f {}
docker run \
    --name $NAME \
    --network bridge \
    --dns $dns_ip \
    -d \
    -v $VOLUME:/etc/openvpn \
    -p 1194:1194/udp \
    --cap-add=NET_ADMIN \
    --restart always \
    kylemanna/openvpn \
    ovpn_run --cipher AES-128-CBC
```

如上所述,我们启动了VPN服务器:
* 注意我们这里是直接用的docker启动,而非放到Kubernetes集群中
* 通过bridge即NAT的方式连接物理服务器,实际上在calico网络模型下,所有的Kubernetes集群内网IP都可以通过物理机访问,所以这里我们用了桥接的方式即可实现访问。
* 绑定到物理机1194的UDP端口上
* 加密算法用的AES-128-CBC

经过上述配置后,我们可以在本地通过netcat访问,如果能成功连接而没有"Connection Refused",就说明服务启动成功了。

```shell
nc -u vpn.coder4.com 1194
```

## 配置客户端

在服务端启动后,我们需要能够从客户端真正的建立OpenVPN隧道。

OpenVPN默认是支持多用户的(即可以有不同用户连接VPN),我们首先要为用户在OpenVPN系统中创建帐号, create_vpn_user.sh:

```shell
#!/bin/bash
 
if [ x"$#" != x"1" ];then
    echo "Usage: $0 <username>"
    exit -1
fi
 
USERNAME="$1"
OVPN_FILE="$USERNAME.ovpn"
CIPHER="AES-128-CBC"
DNS_IP="10.96.0.10"
ROUTE_CMD="route 192.168.0.0 255.255.0.0"
 
VOLUME="$HOME/openvpn"
# generate client cert for username 
docker run -v $VOLUME:/etc/openvpn --rm -it kylemanna/openvpn easyrsa build-client-full $USERNAME nopass
docker run -v $VOLUME:/etc/openvpn --rm kylemanna/openvpn ovpn_getclient $USERNAME > $OVPN_FILE
 
# post process
sed -i 's/redirect-gateway.*$//' $OVPN_FILE
 
cat >> $OVPN_FILE <<EOF
 
# disable lzo
comp-lzo no
 
# add this line, the k8s network route
$ROUTE_CMD
 
# dns update
dhcp-option DNS $DNS_IP 
script-security 2
up /etc/openvpn/update-resolv-conf
down /etc/openvpn/update-resolv-conf
 
# security
cipher $CIPHER 
 
EOF
```

如上执行"create_vpn_user.sh coder4",则会创建一个名为coder4的用户,并生成一个"coder4.ovpn"文件到本地。

我们看一下这个文件:
```
client
nobind
dev tun
remote-cert-tls server

comp-lzo no

remote vpn.coder4.com 1194 udp

<key>
-----BEGIN PRIVATE KEY-----
xxxx
-----END PRIVATE KEY-----
</key>
<cert>
-----BEGIN CERTIFICATE-----
xxxx
-----END CERTIFICATE-----
</cert>
<ca>
-----BEGIN CERTIFICATE-----
xxxx
-----END CERTIFICATE-----
</ca>
key-direction 1
<tls-auth>
#
# 2048 bit OpenVPN static key
#
-----BEGIN OpenVPN Static key V1-----
xxxx
-----END OpenVPN Static key V1-----
</tls-auth>



# add this line, the k8s network route
route 192.168.0.0 255.255.0.0

# dns update
#dhcp-option DNS 10.96.0.10 
script-security 2
up /etc/openvpn/update-resolv-conf
down /etc/openvpn/update-resolv-conf

# security
cipher AES-128-CBC 


```

如上所述:
* 证书部分已经被省略
* 禁用lzo压缩
* 远程服务器地址是vpn.code4.com,协议是udp 
* vpn建立连接后,自动设置路由192.168.0.0/16,这个是Kubernets集群内网的路由
* 应用dns服务器10.96.0.10,这个也是Kubernetes集群默认的

生成文件后,我们在本地网络建立vpn连接:
```shell
openvpn ./coder4.ovpn
```

vpn链接建立成功后,我们尝试ping一个Kubernetes集群内的地址(假设集群内已经启动了若干容器):
```shell
ping 192.168.1.2

PING  (192.168.1.2) 56(84) bytes of data.
64 bytes from 192.168.1.2: icmp_seq=1 ttl=48 time=8.39 ms
64 bytes from 192.168.1.2: icmp_seq=2 ttl=48 time=8.34 ms
64 bytes from 192.168.1.2: icmp_seq=3 ttl=48 time=8.41 ms

```

访问成功!至此,我们通过OpenVPN的方式,成功打通了本地网络和远程Kubernetes集群的内网。

最后,还需要再说明两点:
1. Kubernetes网络模型很多,实现差别很大,本文所述的k8s + calico + docker bridge(nat)的方式 配合才能生效,其他网络模型和组合不保证能成功
1. 由于一些你懂的原因,如果openvpn的server和client之间的互联网跨国了,会被断开或者无法访问,UDP也不行,不过对于微服务的应用场景,影响不大,一般都是服务器部署在国内,开发人员也在国内。

## 拓展与思考
1. 在k8s中,POD与Service IP一般不会分配在同一网段中。如果我们的客户端也想访问Service IP,应当在哪一步、增加哪些配置?
1. 本节通过OpenvVPN实现了从本地(单机)访问Kubernetes集群内网。如果想让本地局域网内,所有机器都可以访问远程Kubernetes集群的内网,应当如何配置呢? 


================================================
FILE: legacy/k8s/README.md
================================================


================================================
FILE: legacy/k8s/docker-k8s.md
================================================
# 集装箱、容器化、容器编排

## 集装箱革命、容器化革命

前面已经提到,微服务架构离不开容器技术。为什么需要容器呢?

我们先来看一个海运的例子。

* 传统海运:尽管货海运已经出现了几千年,但直到20世纪中叶,货物运输依然是一种劳动力密集型工作。码头雇佣了数以万计的工人,将货物从岸上搬运到船舱中。由于货物的种类繁多,体积不一、传送带、铲车都不能根本的解决问题,货物装卸依然大量依赖人工,而且装卸时间大量占用了港口时间,装卸价格居高不下。
* 集装箱革命:20世纪50年代开始,集装箱逐渐走向商用的舞台。货物在岸上按照整齐的规格码放整齐,从而可以封装进集装箱。而装卸货物只需要机械来搬运集装箱即可,极大提高了港口的装卸效率。

集装箱革命使得货物的装卸成本从5美元/吨骤降到16美分/吨,节约了97%...

容器技术的兴起,也成为了运维领域的另一场"集装箱式革命":

- 容器就是集装箱
- 货物则是运行与容器中的、各式应用程序。

容器为运维工作带来了如下革命性进展:

- 容器的标准化:为了让应用程序在生产机上跑起来,经常需要做各种不同操作系统的安装,依赖软件库的安装、环境配置......这些过程非常繁琐,还经常会由于版本的细微差异,和开发环境不一致,出现“这个程序本地好好的,放到服务器上就崩溃”这类情况。容器可以使用统一的描述语言,快速构建出完全相同的、标准化环境,从而解决运行环境的问题。
- 容器的隔离化:此外,不同的应用程序需要不同的应用环境,如果都部署在一台物理机上,很可能会发生包、依赖冲突,导致无法运维,而容器可以为运行在同一台物理机上的应用程序,创建不同的隔离环境。

不仅在运维领域,容器技术在开发环境的搭建、软件教学等领域,也产生了深远的影响。如果你使用过Docker,相信一定对容器带来的变革深有感触,这里就不再一一列举:-)

## 容器、容器编排

如果你应用过容器技术,那么一定听说过该领域内最迟手可热的两个技术:Docker和Kubernetes。

同样都是容器化技术的代表,这两者有什么不同呢?

我们来看一下基本定义:

- Docker:一种容器化引擎
- Kubernetes:一种容器服务的编排系统

Docker的定义比较好理解,它是一种容器的运行时环境。此外,Docker还提供了Dockerfile,这是一种运行环境描述语言。我们可以借助它来快速构建出应用程序所需的、可移植的环境。

Kubernetes的“容器编排”定位,就不是很好理解了,我们先来看一些应用容器技术时遇到的实际问题:

- 如何部署一个应用的多个结点,并进行自动的负载均衡?
- 应用不是孤立的,容器的运行也会有依赖关系,如何进行管理?
- 如何在不影响业务运行的前提下,升级容器(内的应用)?
- 如何在容器应用挂掉的时候,自动恢复故障?

上述问题,已经超越了容器引擎的管理范畴,这些就是“容器编排”系统所要考虑的事情了。

从上述例子我们不难看出,将Docker直接与Kubernetes来进行比较,并不科学,因为他们不是同一个范畴的技术。

实际上,容器引擎有很多种,除了最流行的Docker外,还有[rkt](https://coreos.com/rkt/docs/latest/)、[LXC](https://linuxcontainers.org/)等。从容器编排引擎的角度来看,无论是Docker、rkt还是LXC,都只是运行时引擎,都是可以相互替换的。

类似地,容器编排引擎也有很多种,除了最流行的Kubernetes外,还有[Swarm](https://docs.docker.com/engine/swarm/)、[MESOS](http://mesos.apache.org/)。只不过后两者的发展并不顺利,Kubernetes已经获得了容器编排领域的霸主地位。

前面谈了Docker与Kubernetes的许多不同,但他们也有一些共同点:

- 都是基于容器技术
- 都是采用GO语言编写的
- 都是用yaml作为配置文件格式
- 都是开源社区项目

以Docker为代表的容器技术、以Kubernetes为代表的容器编排技术,也对微服务的发展也起到了推动作用。

试想一下如果你将一个单体服务拆分为10个微服务,部署、运维成本将陡然上升。如何解决这些问题呢?这恰好是容器、容器编排的用武之地。

从容器一下子跳到微服务,你可能还看的有些懵,不要紧,我会在本章接下来的章节为你逐步展开。

## 思考与拓展

1. 如果你想了解更多关于容器、容器编排的事情,可以参考容器周刊上的一篇文章[《Kubernetes vs. Docker: A Primer》](https://containerjournal.com/topics/container-ecosystems/kubernetes-vs-docker-a-primer/)
2. 不同的容器引擎Docker、rkt等,各有什么优劣么?
3. Swarm与Docker出自同一公司。为什么Docker引擎成功了,Swarm编排引擎却被Kubernetes打败了呢?









================================================
FILE: legacy/k8s/helm.md
================================================
# 使用Helm进行包管理

通过前面的章节,我们已经学会了如何在Kubernetes上启动Pod、Deployment及Service。

我们再来简单回顾一下流程过程(以Service为例):

编写yaml文件,其中要包含如下信息:

1. Pod信息
2. Deployment信息
3. Service(ClusterIP)信息

然后通过kubectl应用。

如果你多部署几个Service,就会发现编写yaml是一个非常繁琐的过程,有没有方法可以简化这一操作呢?

答案当然是肯定的,我们可以引入包管理工具Helm。

Helm与Kubernetes的关系,类似于apt与Ubuntu的关系:你当然可以通过编译的方法,手动安装软件;但通过apt install安装软件更加简单方便。

我们先了解下Helm中的几个基本概念:

- Chart:Helm中的软件包,类似于Ubuntu中的deb
- Release:Chart的部署实例,在同一个Kubernetes集群中,同一个Chart可以部署多份。
- Repository:Chart的仓库,可以支持镜像。

下面,跟着我,一起体验Helm的强大之处吧:

## 安装Helm、设置镜像

Helm 3已经正式发布了,最显著的改进是:移除了被人诟病的Tiller,我们以3为例进行讲解。

如无特殊说明,本文的所有操作都在Kubernets的master节点上执行。

首先下载二进制包:

```
wget https://get.helm.sh/helm-v3.0.1-linux-amd64.tar.gz
tar -xzvf helm-v3.0.1-linux-amd64.tar.gz
```

解压缩后,得到helm文件,版本是3.0.1

```bash
./helm version
version.BuildInfo{Version:"v3.0.1", GitCommit:"7c22ef9ce89e0ebeb7125ba2ebf7d421f3e82ffa", GitTreeState:"clean", GoVersion:"go1.13.4"}
```

由于众所周知的原因,我们切换到国内源:

```bash
helm repo add stable http://mirror.azk8s.cn/kubernetes/charts/
```

添加后验证一下:

```bash
./helm repo list
NAME  	URL
stable	http://mirror.azk8s.cn/kubernetes/charts/
```

至此,我们已经完成了helm的配置。

## 使用Helm部署Memcached集群

由于本书还没有涉及Kubernetes的存储部分,我们无需存储的Memcached为例,讲解如何使用helm完成快速部署。

首先搜一下Charts:

```bash
./helm search repo memcached
NAME            	CHART VERSION	APP VERSION	DESCRIPTION
stable/memcached	3.2.1        	1.5.20     	Free & open source, high-performance, distribut...
stable/mcrouter 	1.0.2        	0.36.0     	Mcrouter is a memcached protocol router for sca...
```

我们选择stable的memcached进行部署:

```bash
./helm install test-memcached stable/memcached
```

其中test-memcached是service的名字,默认会部署一个3节点的,有状态的memcached集群

部署成功后输出如下:

```bash
NAME: test-memcached
LAST DEPLOYED: Thu Dec 12 17:46:36 2019
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Memcached can be accessed via port 11211 on the following DNS name from within your cluster:
test-memcached.default.svc.cluster.local

If you'd like to test your instance, forward the port locally:

  export POD_NAME=$(kubectl get pods --namespace default -l "app=test-memcached" -o jsonpath="{.items[0].metadata.name}")
  kubectl port-forward $POD_NAME 11211

In another tab, attempt to set a key:

  $ echo -e 'set mykey 0 60 5\r\nhello\r' | nc localhost 11211

You should see:

  STORED
```

稍等一会,可以看一下Pod状态:

```bash
kubectl get pods
test-memcached-0            1/1     Running            0          25m
test-memcached-1            1/1     Running            0          25m
test-memcached-2            0/1     Pending            0          24m
```

从上图可以发现,2个节点已经启动成功,第3个在Pending,这是因为我的集群中只有2台机器,并且memcached的Charts中规定了每台node上只能启动一台memcached。

如果你直接ping域名“test-memcached.default.svc.k8s.coder4.com”的话,会发现不通,这是因为在master节点上,并没有使用k8s集群内的dns服务器。

我们启动一个临时pod(tiny-tools)来验证一下域名:

```bash
kubectl run -i --tty tiny-tools --image=giantswarm/tiny-tools --restart=Never -- sh
$ ping test-memcached.default.svc.k8s.coder4.com
PING test-memcached.default.svc.k8s.coder4.com (10.36.0.1): 56 data bytes
64 bytes from 10.36.0.1: seq=0 ttl=64 time=0.081 ms
64 bytes from 10.36.0.1: seq=1 ttl=64 time=0.061 ms
64 bytes from 10.36.0.1: seq=2 ttl=64 time=0.061 ms
.....
```

在tiny-tools中,可以ping成功,我们再深入验证下域名解析:

```bash
$ dig test-memcached.default.svc.k8s.coder4.com

; <<>> DiG 9.14.3 <<>> test-memcached.default.svc.k8s.coder4.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 18530
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
; COOKIE: 02680bbf7152f858 (echoed)
;; QUESTION SECTION:
;test-memcached.default.svc.k8s.coder4.com. IN A

;; ANSWER SECTION:
test-memcached.default.svc.k8s.coder4.com. 30 IN A 10.36.0.1
test-memcached.default.svc.k8s.coder4.com. 30 IN A 10.44.0.1

;; Query time: 1 msec
;; SERVER: 10.100.0.10#53(10.100.0.10)
;; WHEN: Thu Dec 12 10:26:21 UTC 2019
;; MSG SIZE  rcvd: 196
```

这个域名解析到了两个POD上:10.36.0.1、10.44.0.1,与前面的启动符合。

如果我们在集群内,想通过域名访问某一台memcached,也是可以的:

```bash
$ ping test-memcached-0.test-memcached
PING test-memcached-0.test-memcached (10.44.0.1): 56 data bytes
64 bytes from 10.44.0.1: seq=0 ttl=64 time=0.053 ms
64 bytes from 10.44.0.1: seq=1 ttl=64 time=0.063 ms
64 bytes from 10.44.0.1: seq=2 ttl=64 time=0.063 ms
.....
```

在Kubernetes集群外(比如master节点上),我们暂时只能通过IP来进行访问:

```
ping 10.44.0.1
PING 10.44.0.1: 56 data bytes
64 bytes from 10.44.0.1: seq=0 ttl=64 time=0.053 ms
64 bytes from 10.44.0.1: seq=1 ttl=64 time=0.063 ms
64 bytes from 10.44.0.1: seq=2 ttl=64 time=0.063 ms
```

至此,借助Helm,我们只需一行命令,就完成了memcached集群的部署。

## 拓展与思考

1. 如何在Kubernetes集群外,通过域名进行访问呢?
1. Helm可以部署有复杂依赖关系的多组服务么?









================================================
FILE: legacy/k8s/k8s-cluster.md
================================================
# 搭建Kubernetes集群

minikube是入门Kubernetes的优秀工具。使用minikube,可以轻松地在本地运行Kuberntes的主要功能。

为了演示方便,本书如有涉及到Kubernetes的章节,多数都是在minikube上运行的。

然而,minikube并不是为生产环境而设计的,当产品真正上线后,是需要部署到真正的Kubernetes集群中的。

在本节中,我们将探讨如何部署真正的Kuberntes集群。

## 环境准备

正所谓“三人成群”,一般来说"集群"需要至少有3台机器。为了方便演示,我们也搭建一台具有3个物理机的Kuberntes集群,1台master,2台slave。

在搭建之前,需要做如下准备:
* 3台物理机安装了Ubuntu 18.04系统[^1]
* 3台物理机具有内网IP,假设为192.168.8.165 ~ 192.168.8.167
* 3台物理设置主机名为k1 ~ k3,通过hostname可以内网互通
* 3台物理机需要联网,但不需要有公网IP

### Docker环境安装

准备好上述条件后,我们在3台机器上分别安装Docker:

```shell
sudo apt-get update && apt-get install -y apt-transport-https
sudo apt install -y docker.io
sudo systemctl start docker
sudo systemctl enable docker
```

如上所述,安装成功后,我们手动启动了docker,并将其加入开机自启动中。

接下来,我们将docker镜像切换到国内源

```bash
sudo vim /etc/docker/daemon.json
```

添加如下内容

```bash
{
    "registry-mirrors": ["https://registry.docker-cn.com"]
}
```

并记得重启

```bash
systemctl restart docker
```

如果你既不是使用root、也没有使用docker用户执行安装,默认是缺少一些权限的,我们添加一下:

```
# 将自己添加到docker组中
sudo groupadd docker
sudo gpasswd -a ${USER} docker
# 重启后重新load下权限
sudo service docker restart
newgrp - docker
```

再强调一次,3台机器上都要执行上述同样的操作。

至此,我们已经准备好了Docker环境。

### Kubernetes的二进制安装

有了Docker后,我们来安装Kubernetes。

由于众所周知的原因,直接使用谷歌的镜像,是没法进行安装的,我们该用aliyun的镜像。

```
sudo /etc/apt/source/apt.list
```

尾部添加如下内容

```
deb http://mirrors.aliyun.com/kubernetes/apt kubernetes-xenial main
```

然后更新源

```
sudo apt-get update
```

报错了

```
W: 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
```

不要慌张,我们只需要修复下PGP,先复制下出错PGP的后6位,然后执行:

```
 gpg --keyserver keyserver.ubuntu.com --recv-keys BA07F4FB
```

然后导入并添加,这次是完整PGP了:

```
gpg -a --export 6A030B21BA07F4FB | sudo apt-key add -
```

之后再更新&安装,应该就不会有问题了

```
sudo apt-get install -y kubelet kubeadm kubectl kubernetes-cni
```

安装后,我们看一下当前版本,目前是1.16.3,这个版本号后面还会用到。

```
kubelet --version Kubernetes v1.16.3
```

安装完毕后,我们需要关闭一下swap

```
sudo swapoff -a
```

注意,上述关闭swap的方法,只针对本次启动有效。

如果想永久关闭swap,需要进行2步操作:

```
sudo vim /etc/fstab
禁用swapfile开头的那一行
```

接着

```
sudo vim /etc/sysctl.conf
# 添加如下行,保存重启
vm.swappiness = 0
```

保存、重启后,就可以了。

记得在3台机器上,都执行上述所有的操作。

### Kubernetes的依赖镜像的准备

有了二进制文件后,还需要依赖的镜像才可以部署集群,这些镜像都在gcr.io上。

由于众所周知的原因,kubernetes启动集群时执行镜像下载会失败,我们需要将镜像提前安装好。

首先将镜像下载到本地,这里我们使用Azure提供的国内镜像(给世纪互联点赞!)

我们首先输入这条awk命令,会输出如下的docker开头的命令,然后执行这些命令:

```
kubeadm config images list --kubernetes-version v1.16.3 | awk -F "/" '{print "docker pull gcr.azk8s.cn/google_containers/"$2""}'

docker pull gcr.azk8s.cn/google_containers/kube-apiserver:v1.16.3
docker pull gcr.azk8s.cn/google_containers/kube-controller-manager:v1.16.3
docker pull gcr.azk8s.cn/google_containers/kube-scheduler:v1.16.3
docker pull gcr.azk8s.cn/google_containers/kube-proxy:v1.16.3
docker pull gcr.azk8s.cn/google_containers/pause:3.1
docker pull gcr.azk8s.cn/google_containers/etcd:3.3.15-0
docker pull gcr.azk8s.cn/google_containers/coredns:1.6.2
```

镜像到本地了,但都是tag的模式,我们需要进行重命名,才能使用,同样的,执行docker开头的输出命令:

```
kubeadm config images list --kubernetes-version v1.16.3 | awk -F "/" '{print "docker tag gcr.azk8s.cn/google_containers/"$2" k8s.gcr.io/"$2""}'

docker tag gcr.azk8s.cn/google_containers/kube-apiserver:v1.16.3 k8s.gcr.io/kube-apiserver:v1.16.3
docker tag gcr.azk8s.cn/google_containers/kube-controller-manager:v1.16.3 k8s.gcr.io/kube-controller-manager:v1.16.3
docker tag gcr.azk8s.cn/google_containers/kube-scheduler:v1.16.3 k8s.gcr.io/kube-scheduler:v1.16.3
docker tag gcr.azk8s.cn/google_containers/kube-proxy:v1.16.3 k8s.gcr.io/kube-proxy:v1.16.3
docker tag gcr.azk8s.cn/google_containers/pause:3.1 k8s.gcr.io/pause:3.1
docker tag gcr.azk8s.cn/google_containers/etcd:3.3.15-0 k8s.gcr.io/etcd:3.3.15-0
docker tag gcr.azk8s.cn/google_containers/coredns:1.6.2 k8s.gcr.io/coredns:1.6.2
```

至此,镜像已经可以直接使用了,但我们可以删除被tag的镜像,节省一些空间:

```
kubeadm config images list --kubernetes-version v1.16.3 | awk -F "/" '{print "docker rmi gcr.azk8s.cn/google_containers/"$2""}'

docker rmi gcr.azk8s.cn/google_containers/kube-apiserver:v1.16.3
docker rmi gcr.azk8s.cn/google_containers/kube-controller-manager:v1.16.3
docker rmi gcr.azk8s.cn/google_containers/kube-scheduler:v1.16.3
docker rmi gcr.azk8s.cn/google_containers/kube-proxy:v1.16.3
docker rmi gcr.azk8s.cn/google_containers/pause:3.1
docker rmi gcr.azk8s.cn/google_containers/etcd:3.3.15-0
docker rmi gcr.azk8s.cn/google_containers/coredns:1.6.2
```

同样地,三台机器都要执行上述操作。

至此,我们已经安装好了部署Kubernetes所需的软件、镜像。

## 集群初始化

在初始化Kuberntes集群前,我们先来解释一些重要参数:
* API Advertise Address:这是Kubernetes Master对外提供交互和操作的接口,一般用内网IP地址
* Pod Network CIDR: 在Kuberntes上运行的所有Pod都需要分配IP地址,会从这个CIDR池中分配。
* Service CIDR: 类似Pod Network,Service分配的IP地址,会从这个CIDR池中分配
* Service DNS Domain: 集群内网的后缀,默认是cluster.local

了解了参数后,我们来初始化集群,在k1上执行:
```shell
sudo 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
```

如上所述:
* 我们选用k1即192.168.8.165这台机器作为master
* pod的cidr是10.200.0.0/16
* service的cidr是10.96.0.0/12 (其实这也是k8s的默认值)
* k8s内网的域名后缀是cluster.coder4

执行过程可能稍长,最后结果大致如下:
```shell
...
Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 192.168.8.165:6443 --token lcyhu1.ie6owlcotmrwiydv \
    --discovery-token-ca-cert-hash sha256:4c345192970992063a0e704ef03ef831a48a368c686047bc32871334292ce091
```

前面打了...的地方,表示还有很长的输出,我们可以不用关心过程,只看最后这些结果。

首先我们按照提示执行配置命令:

```bash
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
```

接着,我们部署网络组件,这里我们选用calico的最新版3.9

```bash
sysctl net.bridge.bridge-nf-call-iptables=1 -w
wget https://docs.projectcalico.org/v3.9/manifests/calico.yaml
sed -i -e "s?192.168.0.0/16?10.200.0.0/16?g" calico.yaml
kubectl apply -f ./calico.yaml
```

注意在上面,我们将calico的cird替换成了我们要使用的10.200.0.0/16,这一步不要忘记。

都执行完毕后,我们查看下集群状态:

```bash
kubectl get nodes
```

应该能输出当前master的状态:

```bash
NAME   STATUS     ROLES    AGE   VERSION
k1     NotReady   master   17s   v1.16.3
```

这里的NotReady是因为还没有Slave加入。

你可能还注意到了最后有一行"join ... token",先复制下来,后面会用到。

## 加入Slave机器

初始化好集群后,加入Slave机器的工作就非常简单了,还记得前面初始化时生成的token命令行么,我们直接拷贝过来,在k2和k3上执行:
```shell
sudo kubeadm join 192.168.8.165:6443 --token lcyhu1.ie6owlcotmrwiydv \
    --discovery-token-ca-cert-hash sha256:4c345192970992063a0e704ef03ef831a48a368c686047bc32871334292ce091
```

加入成功后,我们回到Master机器即k8s1上查看集群状态:
```shell
kubectl get nodes
```

```bash
NAME   STATUS   ROLES    AGE     VERSION
k1     Ready    master   3m37s   v1.16.3
k2     Ready    <none>   105s    v1.16.3
k3     Ready    <none>   91s     v1.16.3
```

可以看到,集群具有3台机器,1个master、2个slave,部署成功!

## 集群重置

有的时候,我们可能执行了错误的命令,或者想直接重建集群。

此时,可以按照如下命令重置集群:

在集群的每台机器上分别执行:

```bash
sudo ipvsadm --clear
sudo kubeadm reset
rm -rf /home/coder4/.kube/
```

[^1]: 这里使用的发行版为Ubuntu最新的LTS版本18.04。其他Linux发型版也是可以,不过后续的安装、配置命令会有略微不同,这里不再赘述。

## 拓展与思考

1. 前面提到了Kubernetes 提供了多种网络模型,他们的区别是什么,各自适用什么业务场景呢?请自行查找资料并回答。
1. 在使用minikube部署Pod时,有时需要创建Volume,我们都是直接创建的本地PV。在正式的Kubernetes集群下,创建Volume有什么不同么?请自行查找资料并回答。


================================================
FILE: legacy/k8s/k8s-ha.md
================================================
# Kubernetes集群的高可用方案

高可用(High Availability)是指系统可以“无中断”地提供服务的能力。

Kubernetes作为容器调度和编排的“操作系统”,高可用显得尤为重要。

在之前的章节,我们搭建的都是“单主集群”,假设master节点挂掉,Kubernetes集群内容器的调度会受到影响

针对“单主集群”、“master节点挂掉”这种情况,Kubernetes已经做了一些处理:已启动的Pod还将继续运行,但挂掉的、新增的Pod将无法被调度。

可见,对于一个要求高可用的线上环境而言,上述的策略是远远不够的。

本节,我们将讨论两种Kubernetes集群的高可用(HA)方案。

## Kubernetes集群的HA方案

构建Kubernetes集群的HA方案有很多种,我们首先介绍如何通过KeepAlived + HAProxy完成HA方案。

我们假设在独立部署的网络环境(非共有云)中有如下的6台主机:

- k1 192.168.8.191 (master)
- k2 192.168.8.187 (master)
- k3 192.168.8.186 (master)
- k4 192.168.8.188
- k5 192.168.8.190
- k6 192.168.8.189
- vip 192.168.8.10

如上所述,k1~k3是主节点,k4~k6是普通节点,而vip(virtual ip)使用ip: 192.168.8.10。

温馨提示,不要在共有云(阿里云、AWS)上尝试本小节的方案。

### 高可用和负载均衡配置

我们首先在主节点安装keepalived:

```bash
sudo apt-get install -y keepalived
```

然后对k1进行配置:

```bash
#sudo nano /etc/keepalived/keepalived.conf
! Configuration File for keepalived

global_defs {
   router_id LVS_DEVEL
}

vrrp_script check_haproxy {
    script "killall -0 haproxy" # check process alive
    interval 3
    weight -2
    fall 10
    rise 2
}

vrrp_instance VI_1 {
    state MASTER          # backup -> BACKUP
    interface eth0        # eth
    virtual_router_id 51
    priority 250          # < 250 for backup server
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass 123456  # please change it for online
    }
    virtual_ipaddress {
        192.168.8.10      # virtual ip not conflict with other
    }
    track_script {
        check_haproxy
    }
}
```

如上所述,我们将k1的keepalived配置了默认master,并将vip绑定到eth0端口上。

对于k2和k3,也要进行类似的配置,我们只需要将"state MASTER"修改为"state slave"即可。

三台机器配置完成后,记得启动服务并设为开机自启动:

```bash
sudo service keepalived start
sudo systemctl enable keepalived
```

我们在k1上查看效果:

```bash
ip address show eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:16:3e:0a:b3:b4 brd ff:ff:ff:ff:ff:ff
    inet 192.168.8.191/24 brd 192.168.8.255 scope global dynamic eth0
       valid_lft 315359417sec preferred_lft 315359417sec
    inet 192.168.8.10/32 scope global eth0
       valid_lft forever preferred_lft forever
```

如上所述,k1上的vip已经自动绑定到了eth0上,当k1挂掉后,k2和k3会竞争master,胜者获得vip。

关于为什么要配置vip,我们稍后会详细讲述。

接着我们安装haproxy:

```bash
sudo apt-get install -y haproxy
```

k1上进行配置:

```bash
sudo nano /etc/haproxy/haproxy.cfg
#---------------------------------------------------------------------
# Global settings
#---------------------------------------------------------------------
global
    # to have these messages end up in /var/log/haproxy.log you will
    # need to:
    #
    # 1) configure syslog to accept network log events.  This is done
    #    by adding the '-r' option to the SYSLOGD_OPTIONS in
    #    /etc/sysconfig/syslog
    #
    # 2) configure local2 events to go to the /var/log/haproxy.log
    #   file. A line like the following can be added to
    #   /etc/sysconfig/syslog
    #
    #    local2.*                       /var/log/haproxy.log
    #
    log         127.0.0.1 local2

    chroot      /var/lib/haproxy
    pidfile     /var/run/haproxy.pid
    maxconn     4000
    user        haproxy
    group       haproxy
    daemon

    # turn on stats unix socket
    stats socket /var/lib/haproxy/stats

#---------------------------------------------------------------------
# common defaults that all the 'listen' and 'backend' sections will
# use if not designated in their block
#---------------------------------------------------------------------
defaults
    mode                    http
    log                     global
    option                  httplog
    option                  dontlognull
    option http-server-close
    option forwardfor       except 127.0.0.0/8
    option                  redispatch
    retries                 3
    timeout http-request    10s
    timeout queue           1m
    timeout connect         10s
    timeout client          1m
    timeout server          1m
    timeout http-keep-alive 10s
    timeout check           10s
    maxconn                 3000

#---------------------------------------------------------------------
# kubernetes apiserver frontend which proxys to the backends
#---------------------------------------------------------------------
frontend kubernetes
    mode                 tcp
    bind                 *:16443
    option               tcplog
    default_backend      kubernetes-apiserver

#---------------------------------------------------------------------
# round robin balancing between the various backends
#---------------------------------------------------------------------
backend kubernetes-apiserver
    mode        tcp
    balance     roundrobin
    server  k1 192.168.8.191:6443 check
    server  k2 192.168.8.187:6443 check
    server  k3 192.168.8.186:6443 check

#---------------------------------------------------------------------
# collection haproxy statistics message
#---------------------------------------------------------------------
listen stats
    bind                 *:1080
    stats auth           admin:awesomePassword
    stats refresh        5s
    stats realm          HAProxy\ Statistics
    stats uri            /admin?stats

```

上述配置比较长,但关键点是将后台的6443端口映射到了haproxy的16443端口上。

经过映射,我们访问haproxy的16443,将会自动负载均衡到k1~k3的某一台的6443端口上。

我们将k2~k3执行同样的配置。

配置完成后,记得启用并设置为开机启动。

```bash
sudo service haproxy restart
sudo systemctl enable haproxy
```

你可能已经发现了,在haproxy的配置中,我们并没有绑定到vip的ip上,而是选择了bind *(绑定全部网络接口)。

虽然k1~k3机器都有16443端口的负载均衡,但如果你想在集群中通过一个固定ip来访问api server,只能使用vip这个固定ip,这也就是为什么要选用vip的原因。

接下来,我们将vip以"host域名"的方式,配置到k1~k6机器的host上:

```bash
cat >> /etc/hosts << EOF
192.168.8.10 k8s-apiserver-vip
EOF
```

至此,所以准备工作已经就绪。

### 多主Kubernetes集群配置

我们在k1上开始Kubernetes集群的配置

```bash
# nano cluster-config.yaml
apiVersion: kubeadm.k8s.io/v1beta1
kind: ClusterConfiguration
kubernetesVersion: v1.16.3
apiServer:
  certSANs:
    - "k8s-apiserver-vip"
controlPlaneEndpoint: "k8s-apiserver-vip:16443"
networking:
  podSubnet: "10.200.0.0/16"
```

上述配置中,我们将api server设置为了api的域名。

启动集群

```bash
sudo kubeadm init --config=./cluster-config.yaml --upload-certs
```

启动成功后,输出

```bash
Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

You can now join any number of the control-plane node running the following command on each as root:

  kubeadm join cluster.coder4:16443 --token vrvld4.vmo52532y6129ktg \
    --discovery-token-ca-cert-hash sha256:3c0268be210a9bd736eebaf80cc6d052b7b317251b15b76b88ddaa223a5836c2 \
    --control-plane --certificate-key 3c1890e1c1ceb1c64c7d1cd002e943e25c89f48c5b3b0e7d84d0879759e54545

Please note that the certificate-key gives access to cluster sensitive data, keep it secret!
As a safeguard, uploaded-certs will be deleted in two hours; If necessary, you can use
"kubeadm init phase upload-certs --upload-certs" to reload certs afterward.

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join cluster.coder4:16443 --token vrvld4.vmo52532y6129ktg \
    --discovery-token-ca-cert-hash sha256:3c0268be210a9bd736eebaf80cc6d052b7b317251b15b76b88ddaa223a5836c2
```

与之前的单master集群相比,这里的输出有两个join命令,其中前者是用于加入其他master节点的,而后者是用于普通节点加入的。

我们先继续k1上的配置

```bash
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
```

配置网络:

```bash
wget https://docs.projectcalico.org/v3.9/manifests/calico.yaml
sed -i -e "s?192.168.0.0/16?10.200.0.0/16?g" calico.yaml
kubectl apply -f ./calico.yaml
```

如果你对上述网络配置不熟悉,请参考[《搭建Kubernetes集群》](k8s-cluster.md)一节中的介绍。

接着在k2和k3执行master节点的加入:

```bash
sudo kubeadm join cluster.coder4:16443 --token vrvld4.vmo52532y6129ktg \
    --discovery-token-ca-cert-hash sha256:3c0268be210a9bd736eebaf80cc6d052b7b317251b15b76b88ddaa223a5836c2 \
    --control-plane --certificate-key 3c1890e1c1ceb1c64c7d1cd002e943e25c89f48c5b3b0e7d84d0879759e54545
```

和配置

```bash
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
```

经过上述配置后,我们检查一下,发现3台master已经成功启动了:

```bash
kubectl get nodes
NAME   STATUS   ROLES    AGE     VERSION
k1     Ready    master   2m47s   v1.16.3
k2     Ready    master   90s     v1.16.3
k3     Ready    master   86s     v1.16.3
```

上述kubectl命令可以在k1~k3任意一台机器执行。

对于k4~k6,我们执行普通节点的加入就好:

```bash
sudo kubeadm join cluster.coder4:16443 --token vrvld4.vmo52532y6129ktg \
  --discovery-token-ca-cert-hash sha256:3c0268be210a9bd736eebaf80cc6d052b7b317251b15b76b88ddaa223a5836c2 \
  --control-plane --certificate-key 3c1890e1c1ceb1c64c7d1cd002e943e25c89f48c5b3b0e7d84d0879759e54545
```

经过上述操作,我们的集群启动完毕:

```bash
kubectl get nodes
NAME   STATUS   ROLES    AGE     VERSION
k1     Ready    master   42m     v1.16.3
k2     Ready    master   37m     v1.16.3
k3     Ready    master   9m39s   v1.16.3
k4     Ready    <none>   3m25s   v1.16.3
k5     Ready    <none>   3m18s   v1.16.3
k6     Ready    <none>   3m15s   v1.16.3
```

为了验证高可用,我们可以试着关闭一下k1,发现虽然节点离线了,但是集群依然可以正常工作,如下所示:

```bash
kubectl get nodes
NAME   STATUS     ROLES    AGE     VERSION
k1     Ready      master   3h37m   v1.16.3
k2     NotReady   master   3h33m   v1.16.3
k3     Ready      master   3m18s   v1.16.3
k4     Ready      <none>   178m    v1.16.3
k5     Ready      <none>   178m    v1.16.3
k6     Ready      <none>   178m    v1.16.3
```

至此,高可用Kubernetes集群已经搭建完毕。

## 共有云下Kubernetes集群的HA方案

在上一小节的开始,我们提到了“不要在共有云尝试上述方案”,为什么呢?

如果你动手尝试过的话,就会发现keepalived在共有云上是行不通的,这是因为:

- 禁用了ssrp,导致ip切换无法广播
- 网卡和ip是存在映射绑定关系的,无法动态添加virtual ip

上述限制主要是出于网络隔离性的安全性考量而产生的。

为了在共有云上部署HA的Kubernetes集群,我们只能另辟蹊径。

事实上,共有云多数提供了负载均衡服务(来替代keepalived),我们将直接使用它来搭建HA集群。

我们假设在阿里云上有6台主机:

- 192.168.8.208 k1(master)
- 192.168.8.207 k2(master)
- 192.168.8.209 k3(master)
- 192.168.8.211 k4
- 192.168.8.212 k5
- 192.168.8.213 k6

其中k1~k3依然作为master节点,其他节点作为普通节点,我们预留了192.168.8.210作为负载均衡的ip。

### 启动HA集群

首先,我们将k1和k3的api-server-lb都指向k1:

```bash
cat >> /etc/hosts << EOF
192.168.8.208   k8s-apiserver-lb
EOF
```

接着在k1上启动k8s集群:

```bash
# nano cluster-config.yaml
apiVersion: kubeadm.k8s.io/v1beta1
kind: ClusterConfiguration
kubernetesVersion: v1.16.3
apiServer:
  certSANs:
    - "k8s-apiserver-lb"
controlPlaneEndpoint: "k8s-apiserver-lb"
networking:
  podSubnet: "10.200.0.0/16"
```

初始化:

```bash
sudo kubeadm init --config=./cluster-config.yaml --upload-certs
```

启动成功过后,也是同样返回了两组节点加入命令:

```bash
Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

You can now join any number of the control-plane node running the following command on each as root:

  kubeadm join k8s-ctl:6443 --token 3kmc18.dhw625244cp3fbc7 \
    --discovery-token-ca-cert-hash sha256:d01e9b50221c690bad534e2ec7b0d6054052f4a016390268730d0b32f387650f \
    --control-plane --certificate-key 849fff7f3df059ed87d91b82f00bdba5988268f9be08f6630eb6d3185438bd7c

Please note that the certificate-key gives access to cluster sensitive data, keep it secret!
As a safeguard, uploaded-certs will be deleted in two hours; If necessary, you can use
"kubeadm init phase upload-certs --upload-certs" to reload certs afterward.

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join k8s-ctl:6443 --token 3kmc18.dhw625244cp3fbc7 \
    --discovery-token-ca-cert-hash sha256:d01e9b50221c690bad534e2ec7b0d6054052f4a016390268730d0b32f387650f
```

我们先完成k1上的配置:

```bash
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
```

初始化网络组件:

```bash
wget https://docs.projectcalico.org/v3.9/manifests/calico.yaml
sed -i -e "s?192.168.0.0/16?10.200.0.0/16?g" calico.yaml
kubectl apply -f ./calico.yaml
```

k2、k3执行master加入操作:

```bash
sudo   kubeadm join k8s-ctl:6443 --token 3kmc18.dhw625244cp3fbc7 \
    --discovery-token-ca-cert-hash sha256:d01e9b50221c690bad534e2ec7b0d6054052f4a016390268730d0b32f387650f \
    --control-plane --certificate-key 849fff7f3df059ed87d91b82f00bdba5988268f9be08f6630eb6d3185438bd7c
```

和配置:

```bash
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
```

接着,我们在阿里云上配置一个私网的负载均衡器,设置如下:

- IP: 192.168.8.210
- 对外暴露TCP 6443端口
- 默认服务器组k1、k2、k3,端口均为6443
- 健康检查间隔调整至最低

配置完成后,稍等几秒钟,执行telnet 负载均衡ip,发现成功连接:

```bash
telnet 192.168.8.210 6443
Trying 192.168.8.210...
Connected to 192.168.8.210.
Escape character is '^]'.


```

接下来,我们在k4~k6配置host为负载均衡的ip:

```bash
cat >> /etc/hosts << EOF
192.168.8.210   k8s-apiserver-lb
EOF
```

然后作为普通节点加入:

```bash
sudo kubeadm join k8s-ctl:6443 --token 3kmc18.dhw625244cp3fbc7 \
    --discovery-token-ca-cert-hash sha256:d01e9b50221c690bad534e2ec7b0d6054052f4a016390268730d0b32f387650f
```

目前在阿里云的负载均衡上有一些限制,当后端服务(k1~k3)自己连接负载均衡ip时,会出现无法连接的情况,而对于其他工作节点(k4~k6)是没有问题的。

对于上述问题,我建议在k1~k3上将上述apiserver修改为自己的ip,即

```bash
# vim /etc/hosts
192.168.8.207~209   k8s-apiserver-lb
```

我们尝试关闭k2节点,然后验证一下高可用,成功:

```bash
kubectl get nodes
NAME   STATUS     ROLES    AGE     VERSION
k1     Ready      master   3h37m   v1.16.3
k2     NotReady   master   3h33m   v1.16.3
k3     Ready      master   3m18s   v1.16.3
k4     Ready      <none>   178m    v1.16.3
k5     Ready      <none>   178m    v1.16.3
k6     Ready      <none>   178m    v1.16.3
```

至此,我们完成了共有云环境下的Kubernetes集群的HA方案。

## 拓展与思考

1. 出于安全性的考虑,Kubernetes集群的节点加入默认只有1个小时的时间窗口,超过这一时间后,如何才能加入集群呢?
2. 除了keepalived、负载均衡,你还知道其他Kubernetes集群的HA方案么?



================================================
FILE: legacy/k8s/k8s-intro.md
================================================
# Kubernetes 快速入门

## Kubernetes中的基本操作单元

为了适应复杂的业务需求,Kubernetes中内置了不同层级的操作单元:
* Pod: Pod是Kubernetes的基本操作单元,也是应用运行的载体。如果你了解Docker的话,可以理解为Pod = 若干紧密相连的Docker + 数据卷。Pod中可能包含若干容器,它们是无法进行更细粒度的分割的,例如:微服务和它的日志收集进程。Pod内部的这些容器共享相同的资源(网络、进程通信、数据卷)
* Replica Set:高可用、高性能是分布式系统中常见的问题。一般都可以采用增加冗余节点的方式解决,Replica Set通过标签关联Pod,并可以设置一个副本数,以实现微服务的冗余。
* Deployment: Deployment描述了一个部署。在Kubernetes中,并不推荐直接启动Pod,也不推荐使用Replica Set,而建议直接使用Deployment。通过在Deployment中描述所期望的Pod、版本和副本数量,就可以实现管理、滚动升级、回滚、扩容、缩容等复杂的操作。Deployment与Pod并非是包含关系,而是相互独立的。Deployment通过“标签匹配”,可以关联若干Pod。
* Service: 从字面意义理解,Service就是服务组。类似的,Service也是独立于Pod、Deployment概念。它也是通过“标签匹配”的方式关联若干Pod,并对外提供了统一的服务代理。通过访问统一服务代理,流量被自动分发到所有关联的Pod上,服务代理可以根据不同策略,进行负载均衡。如果你仔细阅读了[微服务架构概览](architecture/overview.md),就会明白,Kubernetes的Service就是服务发现的一种实现方式。

## minikube
Kubernetes提供了强大的集群管理功能,当然,它的集群环境的配置较为复杂,并非简短篇幅可以说清楚。

本书的核心是微服务架构,而非Kubernetes的使用,因此,我们不会详细讲解k8s集群的配置。

幸运的是,k8s为我们提供了minikube,它一个用于快速开发的单机k8s环境,拥有与k8s集群完全相同的功能。本章的剩余章节,我们将使用minikube来进行讲解。

关于minikube的安装,可以参考官方的这篇[minikube安装教程](https://kubernetes.io/docs/tasks/tools/install-minikube/),这里不做详细展开。

需要特别指出:minikube只限于开发和学习使用。对于生产环境,请务必配置Kubernetes的分布式集群,大家可以参考[官方文档](https://kubernetes.io/docs/home)。

## Hello Deployment

minikube安装妥当后,让我们来部署第一个Deployment。

首先,启动minikube。第一次启动需要下载ISO镜像,时间较长,请耐心等待一下。
```shell
minikube start --disk-size 50g --memory 4096 --insecure-registry "192.168.99.0/24"
```

上述第一次start实际是配置了minikube虚拟机的参数,我们简单解释一下:
* disk-size 磁盘空间我们给了50g。我们之后会配置私有Maven仓库,需要建立主仓库索引,默认的20g不太够用。
* memory 内存,给了4G,可以根据你的需求自己设定。
* insecure-registry 我们之后会搭建不带证书的私有仓库,所以这里预先设置好仓库的IP范围。

提醒一下,上述minikube参数只对第一次启动(实际是创建)生效,一旦虚拟机生成完毕,这些参数的修改都不会生效了。

Kubernetes支持两种操作方式:命令行参数、yaml文件定义。鉴于维护性等角度,我们更推荐推荐后者,即用yaml文件的方式。

Deployment描述文件,lmsia-abc-server-deployment.yaml
```yaml
apiVersion: apps/v1
// Deployment
kind: Deployment
metadata:
  name: lmsia-abc-server-deployment
spec:
  selector:
    matchLabels:
      app: lmsia-abc-server
  replicas: 2
// Pod define
  template:
    metadata:
      labels:
        app: lmsia-abc-server
    spec:
      containers:
      - name: lmsia-abc-server-ct
        image: coder4/lmsia-abc-server:1.0
        ports:
        - containerPort: 8080
        - containerPort: 3000
```

我们来解读一下这个yaml文件,采用自底向上的步骤:
* Pod定义:如注释标记,文件的下半部分定义了Pod信息。
 * metadata.labels.app是Pod的标签名,用于与Deployment、Replica Set和Service做关联。
  * spec.containers定义了Pod的名字(name)、镜像(image)和开放端口(ports)。这里使用了我预先编译好的一个微服务镜像,它集成了REST服务和RPC服务,分别监听8080端口和3000端口。
* Deployment定义:文件的上半部分,是部署的定义。
 * kind类型是Deployment
 * metadata.name是Deployment的名字,用于后续的进一步操作
 * replica是副本数定义,这里我们的副本数(replicas)设定为2。
 * selector.matchLabels定义了与Pod的关联,请注意selector.matchLabels与Pod中的metadata.labels需要保持一致,才能成功关联。

理解了文件内容后,让我们来新建这个部署:
```shell
kubectl apply -f ./lmsia-abc-server-deployment.yaml

```

我们来看看启动了哪些Pod。这里我们以标签为查询参数。
```shell
kubectl get pods -l app=lmsia-abc-server

NAME                                          READY     STATUS    RESTARTS   AGE
lmsia-abc-server-deployment-bd4949ff9-jcczg   1/1       Running   0          10m
lmsia-abc-server-deployment-bd4949ff9-zqsvc   1/1       Running   0          10m
```

不难发现,启动了两个Pod,和我们在yaml文件中设定的副本数一致。

注意:上图展示的是最终结果,Pod的启动前需要先拉取镜像,因此会存在“非Running”的中间状态。

截至目前,我们已经通过Deployment的方式,成功的启动了两个Pod。前面已经介绍,镜像中的微服务对外暴露了两个端口:REST(HTTP)服务的8080端口,和RPC服务的3000端口。接下来,我们尝试访问Pod内的HTTP服务。

先来获取一下IP地址,以第一个Pod为例:

```shell
kubectl describe pod lmsia-abc-server-deployment-bd4949ff9-jcczg

Name:           lmsia-abc-server-deployment-bd4949ff9-jcczg
....
Status:         Running
IP:             172.17.0.5
Controlled By:  ReplicaSet/lmsia-abc-server-deployment-bd4949ff9
....
```

由于结果较多,这里只截取了关键的几行,可以从结果中看到,名字为“lmsia-abc-server-deployment-bd4949ff9-jcczg”的Pod,它的IP是“"172.17.0.5"。

尝试访问一下,会报"No route to host"错误:
```shell
curl http://172.17.0.5:8080/lmsia-abc/api/

curl: (7) Failed to connect to 172.17.0.5 port 8080: No route to host
```
为什么会这样呢?因为我们启动的minikube集群实际是一个虚拟机,172.17.0.5是虚拟机的内网地址。我们执行命令的命令行,是在虚拟机外部。相当与我们从外网要访问内网地址,这自然是无法成功访问的。
要说明的是,我们这里并没有访问跟路径,而是访问了“lmsia-abc/api/”,大家可以暂且认为这是一个合法的url pattern,我们将在微服务开发中对此进行讲解。

如何解决呢,有两种方案:
* 登录到虚拟机上,再访问
* 打通内网和外网
其中,第二种方案是日常工作中常见的需求,我们在OpenVPN + NAT 打通办公网与IDC](devops/openvpn-nat.md)一节中,详细介绍了一种方案。在此处,我们先采用第一种方案。

登录到minikube虚拟机很简单:
```shell
minikube ssh

$

```

注意:若需要登录到minikube虚拟机后再执行的操作,我们会增加一个$符号,以便区分。

登录到minikube虚拟机后,再次尝试Pod上的HTTP服务,可以成功访问了:
```shell
$curl http://172.17.0.5:8080/lmsia-abc/api/

Hello, REST
```

至此,我们成功的创建了Deployment、查看了Pod的信息、访问了Pod上的Rest服务。

最后,我们学习下如何删除Deployment。在虚拟机环境下,是没有kubectl可以使用的,所以首先要退出虚拟机。
```shell
$exit
```

然后再来删除Deployment,命令可以成功执行:
```shell
kubectl delete deployment lmsia-abc-server-deployment

deployment.extensions "lmsia-abc-server-deployment" deleted
```

再来看一下相关的Pod信息,发现已经找不到对应Pod了:
```shell
kubectl get pods -l app=lmsia-abc-server

No resources found.
```


================================================
FILE: legacy/k8s/k8s-ipvs.md
================================================
# 为Kubernetes开启ipvs

通过前面的章节,我们已经学会了如何部署一个真正的Kubernetes集群。

此外,你可能也听说过,Kubernetes内置了Service、Deployment等机制,原生支持了负载均衡。

Kubernetes的负载均衡支持iptables、ipvs两种方案。

两种负载均衡方案相比:ipvs不仅提供了更好的可拓展性,更高的性能,也支持服务器健康检查和重连功能。

然而,Kubernetes默认采用iptables方案,我们在本节探讨如何在k8s中启用ipvs负载均衡。

## 内核模块加载

ipvs依赖一些内核模块,我们先要进行加载

```bash
sudo modprobe ip_vs
sudo modprobe ip_vs_rr
sudo modprobe ip_vs_wrr
sudo modprobe ip_vs_sh
sudo modprobe nf_conntrack_ipv4
```

如果你不想每次都执行这些操作,也可以加入到/etc/modules文件中

## 创建一个Kubernetes集群

我们先使用常规方法创建一个集群,它依然是应用iptables来做负载均衡,稍后我们会修改这一点。

如果你还不知道如何创建集群,可以参考[搭建Kubernetes集群](ms-discovery/k8s-cluster.md)

搭建后,你可以验证一下当前的负载均衡技术,在主节点上执行:

```bash
docker ps | grep proxy
```

获得id后,查看日志,例如我本地的是72a851c1f395

```bash
docker logs -f 72a851c1f395
W1126 11:49:10.647711       1 server_others.go:329] Flag proxy-mode="" unknown, assuming iptables proxy
```

可以看到目前采用的是iptables

## 修改为ipvs负载均衡

我们依然在主节点上执行:

```bash
kubectl edit cm kube-proxy -n kube-system
```

找到"mode"一项,默认应该是空的,修改为"ipvs"。

修改完毕后,不会自动生效,我们手动重启下kube-proxy节点:

```bash
kubectl get pod -n kube-system | grep kube-proxy |awk '{system("kubectl delete pod "$1" -n kube-system")}'
```

稍等几秒钟,所有pod会自动重启,我们验证一下:

```
kubectl get pod -n kube-system |grep kube-proxy
kube-proxy-8zh86             1/1     Running   0          60s
kube-proxy-dp9lj             1/1     Running   0          58s
kube-proxy-zd8xn             1/1     Running   0          52s
```

启动成功后,我们再次找到本地的kube-proxy的docker

```bash
docker logs -f be8714395739
....
I1206 11:03:38.105777       1 node.go:135] Successfully retrieved node IP: 192.168.8.168
I1206 11:03:38.105820       1 server_others.go:176] Using ipvs Proxier.
W1206 11:03:38.106134       1 proxier.go:420] IPVS scheduler not specified, use rr by default
....
```

可以看到,已经成功切换到ipvs负载均衡。

我们也可以通过ipvsadm验证一下:

```bash
sudo ipvsadm
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  k1:https rr
  -> k1:6443                      Masq    1      5          0
TCP  k1:domain rr
  -> 10.32.0.2:domain             Masq    1      0          0
  -> 10.32.0.3:domain             Masq    1      0          0
TCP  k1:9153 rr
  -> 10.32.0.2:9153               Masq    1      0          0
  -> 10.32.0.3:9153               Masq    1      0          0
UDP  k1:domain rr
  -> 10.32.0.2:domain             Masq    1      0          0
  -> 10.32.0.3:domain             Masq    1      0          0
```

## 拓展与思考

1. ipvs支持多种负载均衡策略,在Kubernetes集群中,如何调整这些策略呢?
2. 想较于iptable,为什么ipvs可以提供更好的性能呢?

================================================
FILE: legacy/k8s/k8s-office.md
================================================
# 办公网与Kubernetes集群的打通

通过前面的章节,我们已经学会了Kubernetes集群的搭建、优化、部署应用。

在本节中,我们将讨论另一个常见的场景:打通办公网与Kubernetes集群。

在测试或者开发环境,经常会有这种需求:

- 从开发机直接调用k8s集群中的微服务的rpc接口
- 从开发机直接访问k8s集群中的数据库

然而在默认情况下,办公网络和Kubernetes的pod(service)网络时不互通的。

如果只有少量的HTTP服务,我们可以通过ingress来解决问题。但对于mysql、redis、rpc接口等,就无法通过ingress了,只能用NodePort,当微服务数量不断扩大时,端口将变得难以维护。

对于这种需求,可行的方案并不多,我们将通过vpn来打通办公网和Kubernets集群的网络。

此外,由于绝大多数场景中,只需要从办公网访问Kubernetes集群,所以我们只讨论这种单向打通。

## 从办公网访问Kubernetes集群的Pod IP

首先,你需要有一个Kubernetes集群(calico网络组件),我们假设集群有4个节点:

- k1: 192.168.8.174,主节点
- k2: 192.168.8.175,普通节点
- k3: 192.168.8.176,普通节点
- k4: 192.168.8.177,普通节点(不调度pod)

另外,我们的Kubernetes集群部署在共有云上,至少要保证k4节点有公网IP。

我们查看下状态:

```bash
kubectl get nodes
NAME   STATUS   ROLES    AGE     VERSION
k1     Ready    master   7m35s   v1.16.3
k2     Ready    <none>   72s     v1.16.3
k3     Ready    <none>   48s     v1.16.3
k4     Ready    <none>   37s     v1.16.3
```

我们看一下pod的网段:

```bash
kubectl cluster-info dump | grep -m 1 cluster-cidr
    "--cluster-cidr=10.200.0.0/16",
```

再看一下service的网段:

```
kubectl cluster-info dump | grep -m 1 service-cluster-ip-range
    "--service-cluster-ip-range=10.96.0.0/12",
```

上述两个网段,下面会用到。

我们会将vpn部署在k4上,所以先要打一个标记,不调度Pod到k4上:

```bash
kubectl taint nodes k4 forward=k4:NoSchedule
```

接下来,我们将在k4上部署openvpn。

由于众所周知的原因,我这里不详述openvpn配置了,但请注意一下几点:

- 假设vpn的网络段是192.168.6.0/24
- openvpn可以直接用docker启动,并需要绑定到k4机器的公网ip的端口上

此外,客户端的ovpn配置中,要包含以下几行:

```bash
# For Inner Network (Can Be Done at openwrt also)
route 192.168.6.0 255.255.255.0
route 192.168.8.0 255.255.255.0
route 10.96.0.0 255.240.0.0
route 10.200.0.0 255.255.0.0
```

这4个IP段的配置,分别表示了vpn、k8s物理机、pod ip、service ip。

vpn隧道建立连接后,我们在办公网机器ping一下k8s集群中的pod:

```bash
# ping
ping 10.200.0.2
PING 10.200.0.2 (10.200.0.2): 56 data bytes
64 bytes from 10.200.0.2: icmp_seq=0 ttl=61 time=10.682 ms
64 bytes from 10.200.0.2: icmp_seq=1 ttl=61 time=10.746 ms
64 bytes from 10.200.0.2: icmp_seq=2 ttl=61 time=10.970 ms
```

至此,办公网 -> k8s的IP访问已经打通。

温馨提示:如果你觉得让开发每次挂openvpn不方便:

- 可以将openvpn直接部署到软路由上。
- 如果你的k8s部署在共有云上,也可以使用云厂提供的vp专线(接到出口路由上)替换openvpn。

## 打通DNS

Kubernetes内置了DNS服务,可以通过域名来访问Service。

我们假设通过helm在k8s中部署了一套memcached服务。

首先获取一下dns的地址:

```bash
kubectl  get svc  -n kube-system |grep kube-dns
kube-dns   ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   39m
```

在vpn隧道建立连接后,从办公网尝试直接dig:

```bash
dig @10.96.0.10 test-memcached.default.svc.cluter.coder4

; <<>> DiG 9.10.6 <<>> @10.96.0.10 test-memcached.default.svc.cluter.coder4
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 56004
;; flags: qr aa rd; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;test-memcached.default.svc.cluter.coder4. IN A

;; ANSWER SECTION:
test-memcached.default.svc.cluter.coder4. 30 IN	A 10.42.0.1
test-memcached.default.svc.cluter.coder4. 30 IN	A 10.36.0.1
test-memcached.default.svc.cluter.coder4. 30 IN	A 10.44.0.1

;; Query time: 12 msec
;; SERVER: 10.96.0.10#53(10.96.0.10)
;; WHEN: Tue Dec 17 20:24:21 CST 2019
;; MSG SIZE  rcvd: 237
```

发现可以找到对应的3个后台POD,说明这套DNS基本是可以使用的。

但在实际应用开发中,我们不太可能反复切换本地开发机的DNS服务器地址。

因为,我的建议是:

- 办公网内用dnsmasq搭建一套内网DNS,并打通隧道
- dnsmasq配置,默认走公网dns ip
- dnsmasq配置,对于default.svc.cluster.coder4等k8s集群内的域名,直接走k8s的dns ip

由于这些配置比较常规,这里就不再赘述了。

至此,我们完成了办公网 -> kubernetes集群的网络打通。

## 拓展与思考

1. 如果想做双向打通,如何修改配置呢?
2. 如何用云厂商提供的虚拟专线隧道(VPC),替换openvpn呢?
3. 如果Kubernetes集群部署在办公网内,但属于不同的网段。能否利用双网卡+iptables来替换openvpn呢?

================================================
FILE: legacy/ms-circuit-breaker-and-limit/README.md
================================================
# 微服务熔断与限流

"熔断"、"限流"这两个词看起来是和性能相关的。你可能会有疑问:微服务架构支持横向拓展,性能不够加机器就可以,为什么还需要熔断呢?

是的,横向拓展确实可以提升性能,但同时也降低了可用性。假设一个服务的可用性是99.9%,假设有100个服务实例依赖这个微服务,那么整体的可用性就0.999^100=90%,可用性下降了接近10!考虑到微服务架构下的性能横向拓展,微服务有多个副本的情况下,100个实例的依赖是很正常的情况。

微服务的实际应用中,调用链条可能更长,A调用B、B调用C...链条上的任何一个服务发生故障,都会导致调用链条上后续服务发生故障,从而将故障的影响逐级放大,最终导致整个系统崩溃,这称为雪崩效应。

为了避免雪崩的发生,除了提高服务的稳定性外,还可以采取"熔断"、"限流"等防御性手段。

本章将就这两种手段进行讨论,并引入Hytrix和Guava两款开源解决方案,探讨如何在微服务架构下快速地实现服务的熔断和限流。


================================================
FILE: legacy/ms-circuit-breaker-and-limit/sb-hystrix.md
================================================
# 熔断与Hystrix

在本节,我们将讨论"熔断"方案的思路及其在微服务架构下的落地。

"熔断"这个词来源于电路保护。如果一条线路上的的电压过高,就会将保险丝烧断,从而切断该条线路上的电流,防止其影响其他线路。

我们将上述场景对应到微服务上,当调用某个微服务频繁发生故障(相当于电压过高),会触发熔断(相当于保险丝烧断),微服务将直接返回一个降级的结果,防止影响其他业务。

故障的类型可能有很多种,最常见的是抛出了异常或者调用超时。

你可能会有疑问:返回一个降级的结果,不就是错误了么?

是的,降级结果是错误的。但你可以降低错误的影响范围,例如,返回上一次成功执行的结果。

## Hystrix的基本用法

Hystrix是由Netflex开源的一款开源组件,提供了基础的熔断功能。

Hystrix将降级策略封装在Commend中,不同的Commend根据group分割开。Commend内置了run和fallback两个方法,内置方法。

正常情况下,会先执行run方法(正常执行逻辑),若发生了故障,再执行fallback方法并返回其结果。若发生多次故障会在一定时间范围内触发短路,即跳过run方法,直接执行fallback方法。

关于Hystrix的更详细的原理,可以参考[Hystrix工作原理(官方文档翻译)](https://segmentfault.com/a/1190000012439580),这里不再赘述。

有几个涉及Hystrix的关键参数,这里做一些简要介绍:
首先是几个key
* groupKey: 区分不同降级环境,相同的groupKey下处在相同的降级环境。
* commendKey: 区分不同命令。 
* threadPoolKey: 相同的key将运行在相同的线程池下。
然后是几个通用配置
* executionTimeoutInMilliseconds: 执行超时时间,单位毫秒
* circuitBreakerEnabled: 发生多次故障后,是否会触发短路。默认是false,即总是先执行run方法,不会主动跳过。
最后是线程池
* coreSize: 线程池常驻线程数量
* maximumSize: 线程池最大线程数量
* allowMaximumSizeToDivergeFromCoreSize: 仅当设置为true时,上述maxiumSize才生效。
* maxQueueSize: 最多允许多少个任务堆积
* queueSizeRejectionThreshold: 多少个任务堆积会处罚降级。堆积的任务太多,说明处理速度跟不上需求了,也会被认为是故障并处罚降级。

## Hystrix的基本封装

上述概念固然重要,但每次做熔断时如果都要仔细考虑,未免太过繁琐,为此,我们做了一个抽象的基类,实现了上述的默认的配置:
```java
package com.coder4.lmsia.hystrix;

import com.coder4.sbmvt.trace.TraceIdContext;
import com.coder4.sbmvt.trace.TraceIdUtils;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandProperties;
import com.netflix.hystrix.HystrixThreadPoolKey;
import com.netflix.hystrix.HystrixThreadPoolProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import java.util.function.Supplier;

/**
 * @author coder4
 */
public class BaseHystrixCommend<R> extends HystrixCommand<R> {

    private Logger LOG = LoggerFactory.getLogger(getClass());

    private final Supplier<R> realSupplier;

    private final Supplier<R> fallbackSupplier;

    public BaseHystrixCommend(String key, Supplier<R> realSupplier, Supplier<R> fallbackSupplier) {
        this(key, new BaseHytrixConfig(), realSupplier, fallbackSupplier);
    }

    public BaseHystrixCommend(String key, BaseHytrixConfig config, Supplier<R> realSupplier, Supplier<R> fallbackSupplier) {
        super(Setter
                // 3个key合一
                .withGroupKey(HystrixCommandGroupKey.Factory.asKey(key))
                .andCommandKey(HystrixCommandKey.Factory.asKey(key))
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(key))
                .andCommandPropertiesDefaults(
                        HystrixCommandProperties.Setter()
                                .withExecutionTimeoutInMilliseconds(config.getExecutionTimeoutInMilliseconds())
                                .withCircuitBreakerEnabled(config.isCircuitBreakerEnabled())
                                .withFallbackIsolationSemaphoreMaxConcurrentRequests(config.getFallbackIsolationSemaphoreMaxConcurrentRequests())
                )
                .andThreadPoolPropertiesDefaults(
                        HystrixThreadPoolProperties.defaultSetter()
                                .withAllowMaximumSizeToDivergeFromCoreSize(config.isAllowMaximumSizeToDivergeFromCoreSize())
                                .withCoreSize(config.getCorePoolSize())
                                .withMaximumSize(config.getMaxPoolSize())
                                .withMaxQueueSize(config.getMaxQueueSize())
                                .withQueueSizeRejectionThreshold(config.getMaxQueueSize())
                ));
        this.realSupplier = realSupplier;
        this.fallbackSupplier = fallbackSupplier;
    }

    @Override
    protected R run() throws Exception {
        if (StringUtils.isEmpty(TraceIdContext.getTraceId())) {
            TraceIdContext.setTraceId(TraceIdUtils.getTraceId());
        }
        R r = this.realSupplier.get();
        TraceIdContext.removeTraceId();
        return r;
    }

    @Override
    protected R getFallback() {
        try {
            LOG.error("enter fallback because ", getExecutionException());
            return this.fallbackSupplier.get();
        } finally {
            TraceIdContext.removeTraceId();
        }
    }

}
```

如上所示,在构造函数中,对上述参数进行了配置,此外还结合了[分布式追踪](../ms-log/sb-trace.md)中介绍的TraceIdContext,注入并销毁traceId。

Hystrix的默认值在另一个文件中进行了配置:

```java
package com.coder4.lmsia.hystrix;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author coder4
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BaseHytrixConfig {

    private static int DEFAULT_EXECUTION_TIMEOUT_IN_MILLISECONDS = 1000;

    private static int DEFAULT_FALL_BACK_ISOLATION_SEMAPHORE_MAX_CON_CURRENT_REQUESTS = 512;

    private static int DEFAULT_CORE_POOL_SIZE = 64;

    private static int DEFAULT_MAX_POOL_SIZE = 512;

    private static int DEFAULT_MAX_QUEUE_SIZE = 32;

    // 执行时限(毫秒)
    private int executionTimeoutInMilliseconds
            = DEFAULT_EXECUTION_TIMEOUT_IN_MILLISECONDS;

    // 启动断路器
    private boolean circuitBreakerEnabled = true;

    // (信号量隔离时)降级调用最大并发数
    private int fallbackIsolationSemaphoreMaxConcurrentRequests
            = DEFAULT_FALL_BACK_ISOLATION_SEMAPHORE_MAX_CON_CURRENT_REQUESTS;

    // 允许线程数峰值超过coreSize
    private boolean allowMaximumSizeToDivergeFromCoreSize = true;

    // 核心线程数
    private int corePoolSize = DEFAULT_CORE_POOL_SIZE;

    // 最大线程数量
    private int maxPoolSize = DEFAULT_MAX_POOL_SIZE;

    // 最大队列等待数量
    private int maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
}

```

有了上述默认设置后,我们将上述两个类封装成独立的项目lmsia-hystrix中,方便其他微服务的调用。

## Hystrix在微服务中的用法

最后,我们来看一下如何在微服务中使用hystrix。

首先在依赖中引入lmsia-hystrix:
```
compile 'com.github.liheyuan:lmsia-hystrix:0.0.4'
```

然后定义一个Commend,并execute
```
    @GetMapping(value = "/")
    public String hello() {
        return new BaseHystrixCommend<String>("abc", this::helloReal, this::helloFallback).execute();
    }

    private String helloReal() {
        LOG.info("hello real");
        if (true) {
            throw new RuntimeException("haha");
        }
        return abcLogic.getHello();
    }

    private String helloFallback() {
        LOG.info("hello fb");
        return "Hello, fallback";
    }
```

如上所示,我们定义了两个函数helloReal是正常逻辑,但这个方法会抛异常并触发降级。helloFallback是降级逻辑。

我们构造的Commend会根据情况执行上述两个方法。

在未启用Commend前,rest请求会直接500错误,因为抛出了异常。

启用Commend后,返回总是200。前几次,会发现日志先打印"hello real",再打印"hello fb",这说明只是出发降级未触发短路。当多执行几次,就会发现不再输出"hello real",这时就是真正触发了短路。

至此,我们已经借助Hystrix实现了微服务的降级功能。

## 拓展与思考
1. 返回一个固定的降级结果,可能会影响产品体验。如果想返回上一次执行成功的结果,该如何进行修改呢,Hystrix中有没有内置这个功能呢?
2. Hystrix中默认触发短路的阈值是多少,默认短路时间又是多少呢, 如果想进行修改,需要如何进行配置呢?


================================================
FILE: legacy/ms-circuit-breaker-and-limit/sb-limit.md
================================================
# 限流的实现

与"熔断"类似,"限流"也是一种降级手段,但限流的思路更简单、直观: 它直接拒绝部分请求。

在微服务架构下,若大量请求超过微服务的处理能力时,可能会将服务打跨,甚至产生雪崩效应、影响系统的整体稳定性。

孙子兵法有一计"李代桃僵",说的是当局势发展到必然有所损失时,应当舍得局部弱小兵力,以保全大局优势。

我们可以将这种战略应用到微服务中,在流量超出承受阈值时,直接进行"限流"、拒绝部分请求,从而保证系统的整体稳定性。

有的业务场景中,系统压力并不大,但也需要限制用户每秒的操作次数,例如:验证码的发送接口。

进行限流的方案有很多种,本节这里讨论两个层面上的限流:负载均衡器和微服务。

## 负载均衡层的限流

Nginx是一款高性能的反向代理服务器,是用户请求进入系统的第一道关卡。

在Nginx上配置限流策略,不仅可以保护系统稳定性,也能防范一部分恶意攻击。

我们来看一组最常见的策略。

(1) 按照IP地址, 限制每秒请求数量:

```
limit_req_zone $binary_remote_addr zone=limit1:1m rate=20r/s;
```

如上所述,配置的是一个限流区域:
* 区域名字是limit1,分配1MB的内存,大致可以追踪1.5万个IP地址
* 每个IP地址,每秒钟的访问上限是20次。

(2) 支持突发缓存队列

```
limit_req zone=limit1 burst=10 nodelay;
```

如上所述,细化了limit1这个区域上的具体策略:
* burst建立了一个长度为10的缓冲区,若突发流量导致限流会先放到缓冲区中
* nodelay当缓冲区已满了,丢弃请求,返回503

上面提到的缓存策略可以应用于全局,也可以应用于不同的url路径下。

此外,Nginx还提供了多种高级的限流配置手段,可以参考这篇博客[Nginx Rate Limiting](https://dzone.com/articles/nginx-rate-limiting)。

## 微服务层的基础限流

由于Nginx无法解析业务逻辑,只能在IP层面进行"较为粗犷的限流"。

如果想结合业务逻辑或更复杂的策略,可以在微服务层面进行限流。

Guava是谷歌开源的Java库,其中提供了基于令牌桶算法的RateLimiter。我们将以此为基础,实现微服务层面的限流。

首先,来定义一个注解类
```java
package com.coder4.lmsia.ratelimit;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 对方法限流,超限会抛出HTTP 429异常
 * @author coder4
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface MethodRateLimit {

    // 每秒允许多少次请求
    double permitsPerSecond();
}
```

如上所述,注解定义了一个参数permitsPerSecond,即每秒允许几次请求,支持非整数配置。

为了让注解生效,我们需要配合AOP使用:
```java
package com.coder4.lmsia.ratelimit.aspect;

import com.coder4.lmsia.commons.http.exception.Http429TooManyRequestsException;
import com.coder4.lmsia.ratelimit.MethodRateLimit;
import com.coder4.lmsia.ratelimit.RateLimiterProvider;
import com.google.common.util.concurrent.RateLimiter;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Optional;

/**
 * @author coder4
 */
@Component
@Aspect
public class MethodRateLimitAspect {

    protected Logger LOG = LoggerFactory.getLogger(getClass());

    @Around(value = "(execution(* com.coder4..*(..))) && @annotation(methodLimit)", argNames = "joinPoint, methodLimit")
    public Object methodAround(ProceedingJoinPoint joinPoint, MethodRateLimit methodLimit)
            throws Throwable {
        // Get RateLimiter
        Optional<RateLimiter> rateLimiterOp = RateLimiterProvider.getInstance()
                .getRateLimiter(
                        joinPoint.getSignature().toLongString(), methodLimit.permitsPerSecond());
        if (!rateLimiterOp.isPresent() || rateLimiterOp.get().tryAcquire()) {
            // allow
            return joinPoint.proceed();
        } else {
            // deny
            throw new Http429TooManyRequestsException();
        }
    }

}
```

如上所述,我们对所有添加了MethodRateLimit注解的方法进行AOP注入:
* 根据方法名获取一个RateLimiter,RateLimiterProvider稍后会介绍
* 若可以获得令牌,则执行方法,否则抛出HTTP429(Too Mangy Requests)异常

再来看一下RateLimiterProvider:
```java
package com.coder4.lmsia.ratelimit;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.util.concurrent.RateLimiter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * @author coder4
 */
public class RateLimiterProvider {

    private Logger LOG = LoggerFactory.getLogger(getClass());

    private static final RateLimiterProvider instance = new RateLimiterProvider();

    private static final int CAPACITY = 2000;

    private static final int TTL_SECS = 60;

    private Cache<String, RateLimiter> rateLimiterCache;

    private RateLimiterProvider() {
        rateLimiterCache = CacheBuilder.newBuilder()
                .maximumSize(CAPACITY)
                .expireAfterAccess(TTL_SECS, TimeUnit.SECONDS)
                .build();
    }

    public static RateLimiterProvider getInstance() {
        return instance;
    }

    public Optional<RateLimiter> getRateLimiter(String key, double permitsPerSecond) {
        // 未测试线程安全,但影响不大
        try {
            return Optional.ofNullable(
                    rateLimiterCache.get(key, () -> RateLimiter.create(permitsPerSecond)));
        } catch (ExecutionException e) {
            LOG.error("getRateLimiter exception", e);
            return Optional.empty();
        }
    }

}
```

如上所述,Provider的内部使用Guava的Cache机制:
* 根据字符串key从Cache中尝试获取RateLimiter,获取不到则新建一个
* Cache最高存储2000个、过期时间为60秒,以防不断膨胀导致过高的内存开销。

有了上述注解,在微服务中进行限流将异常简单:
```java
    @MethodRateLimit(permitsPerSecond = 2.0)
    @GetMapping(value = "/")
    public String hello() {
        return new BaseHystrixCommend<String>("abc", this::helloReal, this::helloFallback).execute();
    }
```

如上,只需要一行代码即可搞定。

## 微服务层的高级限流

在一些复杂的业务场景下,我们可能希望根据不同用户或其他字段进行限流。

我们提供了另一款MethodParamRateLimit来满足这类需求:
```java
package com.coder4.lmsia.ratelimit;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 根据方法+参数限流,超限会抛出HTTP 429异常
 *
 * @author coder4
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface MethodParamRateLimit {

    // 每秒允许多少词请求
    double permitsPerSecond();

    // 参数下标(0开始)
    int paramIndex();
}
```

新增的参数paramIndex稍后会作出解释,我们看一下AOP的Aspect:
```java
package com.coder4.lmsia.ratelimit.aspect;

import com.coder4.lmsia.commons.http.exception.Http429TooManyRequestsException;
import com.coder4.lmsia.ratelimit.MethodParamRateLimit;
import com.coder4.lmsia.ratelimit.RateLimiterProvider;
import com.google.common.util.concurrent.RateLimiter;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Optional;

/**
 * @author coder4
 */
@Component
@Aspect
public class MethodParamRateLimitAspect {

    protected Logger LOG = LoggerFactory.getLogger(getClass());

    @Around(value = "(execution(* com.coder4..*(..))) && @annotation(methodParamLimit)", argNames = "joinPoint, methodParamLimit")
    public Object methodAround(ProceedingJoinPoint joinPoint, MethodParamRateLimit methodParamLimit)
            throws Throwable {
        // Get RateLimiter
        Optional<RateLimiter> rateLimiterOp = RateLimiterProvider.getInstance()
                .getRateLimiter(getRateLimiterKey(joinPoint, methodParamLimit), methodParamLimit.permitsPerSecond());
        if (!rateLimiterOp.isPresent() || rateLimiterOp.get().tryAcquire()) {
            // allow
            return joinPoint.proceed();
        } else {
            // deny
            throw new Http429TooManyRequestsException();
        }
    }

    private String getRateLimiterKey(ProceedingJoinPoint joinPoint, MethodParamRateLimit methodParamLimit) {

        // Get Param Value
        String paramValue = getParamLimit(joinPoint, methodParamLimit.paramIndex());

        return String.format("%s-%s", joinPoint.getSignature().toString(), paramValue);
    }

    private String getParamLimit(ProceedingJoinPoint joinPoint, int paramIndex) {
        Object[] args = joinPoint.getArgs();
        if (paramIndex < 0 || paramIndex >= args.length) {
            LOG.warn("paramIndex exceed length, use default");
            return "default_param";
        }
        return args[paramIndex].toString();
    }

}
```

如上所述,进行切面处理时:
* 从用方法和第paramIndex参数的值拼接为key来获取RateLimit。这有些抽象,我们稍后会举个例子。
* 其他处理策略同MethodLimitAspect

看一下用法:
```java
    @MethodParamRateLimit(permitsPerSecond = 1, paramIndex = 0)
    @GetMapping(value = "/ids/{id}")
    public String helloWithId(@PathVariable int id) {
        return helloFallback(id);
    }
```

如上所述,MethodParamRateLimit应用在此处,实现了根据不同的id进行限流,每个id每秒只能访问1次,不同id之间不会相互影响。

## 阅读与思考
1. Nginx进行限流时,容易发生误伤,例如来自内网或者监控系统的IP。请自行查找资料,实现白名单配置,避免这种情况。
2. 除了负载均衡、微服务层面的限流,你还能想到其他层面的限流么?


================================================
FILE: legacy/ms-config/README.md
================================================
# 微服务配置中心

"配置中心"是对配置进行集中管理的系统,是微服务架构中的一个基础环节。

服务端存在多种类型的配置:
* 环境变量:如操作系统环境变量
* 内部配量:如阈值、关键字
* 应用配量:如功能、特性开关

环境变量等配置很少发生变动,但内部、应用配置可能会频繁发生变动。

传统的配置都是写在文件中,并伴随服务端一起发布,这样做有一些弊端:
* 配置文件格式多样,易产生错误。例如,yaml和ini的格式就极容易混淆,导致配置出错。
* 不同环境下配置容易混淆。例如,误将测试环境配置发布到生产环境。
* 每次配置的修改都要伴随服务端的上线。在微服务架构下,服务种类、实例数量增多,为了改动配置而单独上线会产生较大的运维成本。

针对这些场景,配置中心应运而生,它的核心功能有:
* 提供配置的存、取服务
* 支持配置的动态更新,即无需启动服务即可完成配置更新。
* 支持配置的版本管理或追溯功能,方便进行审计。

本章将围绕配置中心展开讨论,在第一节讨论了一种基于cfg4j的配置中心方案。第二节讨论了如何在Spring Boot中整合上述配置中心。


================================================
FILE: legacy/ms-config/cfg4j.md
================================================
# cfg4j及方案简介

实现微服务的配置中心有多种选择方案,常见的方案有:
* 使用Spring Cloud全家桶中的Spring Cloud Config。
* 使用Consul或者Zookeeper作为分布式一致性存储,自己实现配置中心。

但上述方案都有一些不足:
* Sping Cloud Config不支持配置的实时更新,需要额外实现。此外,Spring Cloud的依赖较多,不太干净。
* Consul或者Zookeeper只提供了存取接口,对于配置下发、更新(特别是配置的管理界面)都需要自己开发实现,成本非常高。

我们在此选用了一种成本较低的方案: 使用cfg4j库,存储源选择git。

cfg4j是一款"分布式系统"的配置类库,它不包含服务端存储部分,但实现了从多种数据源读取,更新配置,以及缓存策略。

我们选用git作为数据源,原因有:
* 本书架构本身采用git作为代码管理工具,不需要额外部署成本
* 本书已经采用了gerrit作为git服务器和代码审核工具,它的diff、review功能非常强大,可以不用额外开发配置管理的web界面

## gerrit中的权限配置

截至目前的最新版,cfg4j默认只支持从匿名git仓库拉取配置,我们需要对gerrit进行一些配置以满足这一条件。

使用管理员帐号登录gerrit,然后选择"Projects" -> "All-Projects" -> "Access",进行如下修改:
* 给"Reference: refs/*" 添加匿名组 "Anonymous Users" 的 "DENY"权限。注意,这只是一个全局的默认配置,可以被项目级别的权限覆盖。
* 新建项目"lmsia-config",修改项目权限,给"Reference: refs/*"添加匿名组 "Anonymous Users" 的 "ALLOW"权限。

经过上述修改后,我们需要做一下简单验证:
```shell
git clone http://127.0.0.1:9002/lmsia-config
```

如果上述命令可以直接clone项目到本地,且无需输入用户名、密码,说明gerrit的权限配置成功。

## 支持多个微服务的配置

在前面,我们新建了项目lmsia-config,并且给它配置了匿名访问权限。

我们的微服务可能有很多,如何让lmsia-config项目,支持多个微服务的配置呢?

我们通过目录的方式来实现:
```shell
.
├── lmsia-abc
│   └── config.properties
└── lmsia-xyz
    └── config.properties
```

如上所示,对于每一个微服务项目,我们都在lmsia-config下创建一个目录,并在目录中放置config.properties作为配置文件。

在后面的章节,我们将介绍如何让微服务自动地解析上述配置文件路径。

## 拓展与思考
1. 如果测试环境、线上环境需要使用不同的配置,如何支持这种特性?(提示:分支)
2. 如果希望匿名用户只读而不能写,如何修改gerrit权限?


================================================
FILE: legacy/ms-config/consul-devops.md
================================================
# Consul服务的运维

Consul是一款支持高可用的服务发现、配置管理服务。我们使用Consul作为配置中心的基础服务。即由Consul提供配置的管理、获取等基础功能。

在探讨配置中心之前,我们首先来看一下Consul的运维工作。

## 生成Consul所需要的证书

本文配置生成的部分参考了[consul-on-kubernetes](https://github.com/kelseyhightower/consul-on-kubernetes)项目。

首先生成中证书

```shell
~/go/bin/cfssl gencert -initca ca/ca-csr.json | ~/go/bin/cfssljson -bare ca

~/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
```

生成的文件为:
* ca-key.pem
* ca.pem
* consul-key.pem
* consul.pem

然后根据证书生成kubernetes所需要的configmap
```shell
kubectl create secret generic consul --from-literal="gossip-encryption-key=X9u61NBsxoQt6edwxpStLg==" --from-file=ca.pem --from-file=consul.pem --from-file=consul-key.pem

kubectl create configmap consul --from-file=configs/server.json
```

其中上述密码部分是由“consul keygen”生成的,可以改成自己的密码。


================================================
FILE: legacy/ms-config/sb-config.md
================================================
# Spring Boot整合配置中心

上一小节中,我们探讨了如何利用gerrit搭建配置中心的版本仓库。

现在,我们探讨如何在Spring Boot的框架中整合配置中心。

## 开发lmsia-cfg4j库,实现配置项的自动注入

与之前的Cache等功能类似,我们在多个微服务中,轻松地使用配置中心,所以将相关功能提取但独立的项目中。你可以在这里查看[lmsia-cfg4j](https://github.com/liheyuan/lmsia-cfg4j)的源代码。

如前文描述,我们使用cfg4j来辅助实现配置中心的功能。

cfg4j提供了默认的"Binding"方式,来绑定配置项到类中,但使用起来较为繁琐。

许多Spring的功能都是通过注解来实现的,非常方便。我们的配置项也可以用注解来实现,首先定义一个注解接口:

```java
package com.coder4.lmsia.cfg4j;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author coder4
 */
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cfg4jValue {

    String value() default "";
}

```

如上所示,之后希望可以使用类似"@Cfg4jValue"的方式,将配置项注解到对应字段中。

有了注解接口,如何实现自动注解呢,传统的方式需要使用动态代理来完成,在这里我们采用Spring提供的BeanPostProcessor来完成:
```java
package com.coder4.lmsia.cfg4j;

import org.cfg4j.provider.ConfigurationProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.util.NoSuchElementException;

/**
 * @author coder4
 */
public class Cfg4jValueProcessor implements BeanPostProcessor, Ordered {

    private Logger LOG = LoggerFactory.getLogger(getClass());

    @Autowired
    private ConfigurationProvider configurationProvider;

    // 初始化前注入
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

        final Class targetClass = AopUtils.getTargetClass(bean);
        ReflectionUtils.doWithFields(targetClass, field -> process(bean, targetClass, field), field -> {
            return field.isAnnotationPresent(Cfg4jValue.class);
        });
        return bean;
    }

    private void process(final Object bean, Class<?> targetClass, final Field field) {
        // Get injected field name
        Cfg4jValue valueAnnotation = field.getDeclaredAnnotation(Cfg4jValue.class);
        String fieldName = getPropName(valueAnnotation, field.getName());
        // inject for some support type
        fieldSetWithSupport(bean, field, fieldName);
    }

    private void fieldSetWithSupport(Object bean, Field field, String key) {
        Class type = field.getType();
        field.setAccessible(true);
        try {
            if (int.class == type || Integer.class == type) {
                field.set(bean, configurationProvider.getProperty(key, Integer.class));
            } else if (boolean.class == type || Boolean.class == type) {
                field.set(bean, configurationProvider.getProperty(key, Boolean.class));
            } else if (String.class == type) {
                field.set(bean, configurationProvider.getProperty(key, String.class));
            } else if (long.class == type || Long.class == type) {
                field.set(bean, configurationProvider.getProperty(key, Long.class));
            } else {
                LOG.error("not support cfj4j value inject type");
                throw new RuntimeException("not supported cfg4jValue type");
            }
        } catch (IllegalAccessException e) {
            LOG.error("exception during field set", e);
            throw new RuntimeException(e);
        } catch (NoSuchElementException e) {
            LOG.error("config missing key, please check");
            throw new RuntimeException(e);
        }
    }

    public static String getPropName(Cfg4jValue annotation, String defaultName) {
        String key = annotation.value();
        if (key == null || key.isEmpty()) {
            key = defaultName;
        }
        return key;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws
            BeansException {
        return bean;
    }

    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE;
    }
}
```

如上所示,Cfg4jValueProcessor完成了以下功能:
* 自动查找所有@Cfg4jValue的注解
* 对有上述注解的字段,根据字段名从Cfg4j的数据源(ConfigurationProvider)中读取配置项
* 若有配置项,完成类型转换并注入到对应字段中。这里目前只支持int, long, string这三种类型。

ConfigurationProvider是cfg4j的数据源,如前文所述,我们希望它自动从gerrit来读取。

为此,实现一个自动配置如下:
```java
package com.coder4.lmsia.cfg4j.configuration;

import com.coder4.lmsia.cfg4j.Cfg4jValueProcessor;
import org.cfg4j.provider.ConfigurationProvider;
import org.cfg4j.provider.ConfigurationProviderBuilder;
import org.cfg4j.source.ConfigurationSource;
import org.cfg4j.source.context.environment.Environment;
import org.cfg4j.source.context.environment.ImmutableEnvironment;
import org.cfg4j.source.context.filesprovider.ConfigFilesProvider;
import org.cfg4j.source.git.GitConfigurationSourceBuilder;
import org.cfg4j.source.reload.ReloadStrategy;
import org.cfg4j.source.reload.strategy.PeriodicalReloadStrategy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;

import java.nio.file.Paths;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;

/**
 * @author coder4
 */
@Configuration
@ConditionalOnProperty("msName")
public class Cfg4jGitConfiguration {

    @Value("${msName}")
    private String msName;

    // May Change this
    private static String CONFIG_GIT_HOST = "10.1.64.72";

    // May Change this
    private static String CONFIG_GIT_REPO = "http://" + CONFIG_GIT_HOST + ":9002/lmsia-config.git";

    // May Change this
    private static String branch = "master";

    private static int RELOAD_SECS = 60;

    @Bean
    public ConfigurationProvider configurationProvider() {
        ConfigFilesProvider configFilesProvider = () -> Arrays.asList(Paths.get(msName + "/config.properties"));
        ConfigurationSource source = new GitConfigurationSourceBuilder()
                .withRepositoryURI(CONFIG_GIT_REPO)
                .withConfigFilesProvider(configFilesProvider)
                .build();

        Environment environment = new ImmutableEnvironment(branch);

        ReloadStrategy reloadStrategy = new PeriodicalReloadStrategy(RELOAD_SECS, TimeUnit.SECONDS);

        return new ConfigurationProviderBuilder()
                .withConfigurationSource(source)
                .withEnvironment(environment)
                .withReloadStrategy(reloadStrategy)
                .build();
    }

    @Bean
    public Cfg4jValueProcessor createCfg4jValueProcessor() {
        return new Cfg4jValueProcessor();
    }

}
```

上述完成了如下功能:
* 从Git仓库拉去lmsia-config项目(即前文用于微服务配置的仓库)
* 定义缓存时间为60秒
* 配置文件具体路径为/项目名/config.properties(与前一节的目录结构相对应)
* 顺便配置刚才编写的Cfg4jValueProcessor,让配置可以自动注入到对应的地方上。

当然,为了让上述自动注解生效,不要忘记配置spring.factories
```
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.coder4.lmsia.cfg4j.configuration.Cfg4jGitConfiguration
```

至于项目名,可以通过微服务自身的application.yaml指定,若不制定将不会启动这个自动配置

## 使用

有了lmsia-cfg4j后,如何在微服务中自动注入配置项目呢?

首先我们需要准备好配置,例如lmsia-config/lmsia-abc的config.properties中定义
```
key=value
enable=false
```

接着,在微服务的application.yaml中定义项目名称
```yaml
msName: lmsia-abc
```

最后一步,在代码中,使用注解:
```java
package com.coder4.lmsia.abc.server.configuration;

import com.coder4.lmsia.cfg4j.Cfg4jValue;
import lombok.Data;
import org.springframework.stereotype.Service;

/**
 * @author coder4
 */
@Service
@Data
public class TestConfig {

    @Cfg4jValue
    private String key;

    @Cfg4jValue
    private boolean enable;

}
```

如上所示,是不是非常简单!

你可以启动自己的微服务项目,测试上述配置项是否被如期的注入进来。

在lmsia-cfg4j中,有默认60秒的缓存,你也可以修改lmsia-abc的配置,等待60秒,再观察新的配置是否生效。

## 拓展与思考

1. 我们介绍的的配置中心架构中,实际采用的是拉默认来获取最新配置。当微服务及其副本数量众多的时候,可能会对gerrit服务器造成巨大压力。有什么好的方法可以改进这一点么?
2. 如果需要结合profile实现测试、线上环境使用不同的配置,lmsia-cfg4j项目要如何进行修改呢? 


================================================
FILE: legacy/ms-delivery/README.md
================================================
# 微服务持续交付

"持续集成"(Continuous integration):频繁地将开发代码合并到主干,并保证可以编译通过,并通过基本额单元测试。
坚持持续集成的优点有:
* 快速失败(Fast Fail): 尽早发现错误,"早发现、早治疗",将错误的成本降到最低。
* 减少代码冲突风险:使用频繁、多次、小的分支合并,来很久不合并代码导致的大量的代码冲突。
* 提升迭代速度和质量:小步快跑,让进度更可控,避免"Deadline前赶工期"的现象。

"持续交付"(Continuous delivery):频繁地将代码的最新版本,交付给用户(或线上环境),它的优势不言而喻:
* 更快的交付速度:借助自动化的持续交付系统,可以做到1天上线多次
* 产品功能可并行上线:持续交付降低了上线的人力成本,多个功能可并行开发、上线

在互联网软件开发领域,持续集成和持续交付已经成为基本的共识,极大地提升了项目的迭代、交付速度。

与之形成鲜明对比的是,在传统软件开发领域,持续集成和持续交付的理念还没有得到贯彻,软件的开发、上线以周、月为单位,并且功能之间往往难以进行拆分。

本章将围绕上述两个问题,探讨探讨微服务架构下,借助Jenkins实现持续集成、持续部署。


================================================
FILE: legacy/ms-delivery/jenkins-devops.md
================================================
# Jenkins构建平台的运维

Jenkins是一款开源的持续构建工具,除了基础功能外,还有各种功能丰富的插件,可以实现各种高级功能。

Jenkins常见的应用场景是:
* 项目的自动构建(编译),即持续集成
* 自动执行项目的单元/集成测试,即持续测试
* 实现项目的自动部署、上线,即持续部署

在本小节,我们首先探讨Jenkins系统的运维工作,并尝试将Jenkins与LDAP系统集成起来。

## Jenkins系统的搭建

对于Jenkins系统,我们将直接用Docker部署在物理机而不是k8s集群上,主要原因有:
* Jenkins的定位是持续集成、持续部署,需要作用在k8s集群上,故耦合不宜太紧密。
* 由于资源有限,我们的k8s集群主要运行微服务及相关后台组件。

我们先来看一下创建脚本
```shell
#!/bin/bash
NAME="jenkins"
VOLUME="$HOME/docker_data/jenkins"

# ensure volume ready
sudo mkdir -p $VOLUME
sudo chmod -R 777 $VOLUME

# submit to local docker node 
docker ps -q -a --filter "name=$NAME" | xargs -I {} docker rm -f {}
docker run \
    --name $NAME \
    -v $VOLUME:/var/jenkins_home \
    -p 9001:8080 \
    -p 50000:50000 \
    --detach \
    --restart always \
    jenkins/jenkins:2.60.3-alpine
```

如上所述:
* 我们使用了jenkins的官方镜像
* 映射默认端口到本地的9001,这个即web管理界面的端口
* 映射端口50000,这个是用于管理通信的端口
* volume映射了/var/jenkins_home文件夹

启动完毕后,需要进行Jenkins系统的初始化,如下图所示:

![Jenkins初始化](./jenkins-init.png)

初始化需要输入初始化密码,存放在docker的/var/jenkins_home/secrets/initialAdminPassword目录下,可以通过docker exec -i -t 登录容器,然后cat查看。
```shell
docker exec -i -t ad74be122fcd /bin/sh

cat /var/jenkins_home/secrets/initialAdminPassword
0b7aee7513774800ac6cf1fdd41d0366
```

输入密码后,稍等一会儿,提示选择安装插件的模式,选择自定义插件:

![Jenkins插件选项初始化](./jenkins-init-plugin.png)

针对本书架构,建议选择的插件为:
* Organization and Administration
 * Folder
* Build Features
 * Build Timeout
 * SSH Agent
 * Timestamper 
 * Workspace Cleanup 
 * Active Choice
* Build Tools
 * Gradle
* Pipelines and Continuous Delivery
 * Pipeline
 * Pipeline: Stage View
* Source Code Management
 * Git
* Distributed Builds 
 * SSH Slaves
* User Management and Security
 * LDAP
 * Role-based Authorization Strategy
* Notifications and Publishing
 * Publish Over SSH
 * SSH

选择好后进行安装,这一步会耗费的久一些,随后需要创建管理员帐号:

![Jenkins创建管理员帐号](./jenkins-init-admin.png)

最后点击完成

经过上述配置后,我们已经构建了基本的Jenkins环境,并有了一个可以登录的系统管理员帐号。

## Jenkins系统接入LDAP系统

作为持续集成的核心,Jenkins系统的重要性毋庸置疑,有必要进行细粒度的权限控制与管理,例如:
* 开发用户可以修改、执行Job,但不能新建项目
* 测试用户只能运行Job
* 管理员可以进行任何操作

因此,我们需要给每个团队成员配置独立的帐号。面对这种需求,接入LDAP验证是一个很好的选择。

在[LDAP 内部账号管理系统](toolchain/ldap.md)一节中,我们介绍了LDAP的运维方案,本节假设你已经部署好了LDAP服务。

登录Jenkins后,在左侧菜单选择"Manage Jenkins",随后选择"Configure Global Security"

在"Access Control"中选中"LDAP",填写如下信息:
* Server: LDAP服务和端口号,例如 ldap.coder4.com:389
* Root DN: LDAP的根目录,例如 dc=coder4,dc=com
* User search base: 用户所在的子目录,例如 ou=users
* User search filter: 用户名的字段,例如cn={0}
* Manager DN: 可查询LDAP的额外帐号,例如一个只读帐号 cn=guest,dc=coder4,dc=com
* Manager Password: 上述帐号对应的密码
* Display Name LDAP attribute: 帐号名字段名,例如cn
* Email Address LDAP attribute: 邮箱字段名,例如mailk

上述都填写完毕后,可以点击底部的Test LDAP Settings

测试通过后,不要忘记点击页面最底部的保存按钮。

随后,我们尝试用LDAP帐号登录,可以发现登录成功。

## 设置基于角色的权限

团队成员有多种不同的角色,如前面提到的开发、测试、管理员。

在Jeknins中,可以设置不同的角色,并针对角色配置不同的权限。

点击"Manage Jenkins",然后选择"Configure Global Security",在"Authorization"中更改为"Role Based Strategy"。

不要退出Jenkins,选择"Manage and Assign Roles",首先管理角色"Manage and Assign Roles"

![Jenkins分配系统角色](./jenkins-role.png)

如上图所示,新增2个角色:开发rd和测试qa,并设置对应的权限。

随后进入"Assign Roles",将开发帐号赋给对应的角色,这里给lihy赋予admin角色、给zhangsan赋予rd角色,如图所示:

![Jenkins赋予系统角色](./jenkins-assign-role.png)

我们可以登录zhangsan,没有看到"Manage Jenkins"的菜单,角色配置成功。



================================================
FILE: legacy/ms-delivery/ms-cd.md
================================================
# Jenkins持续部署

在上一小节,我们完成了Jenkins的持续集成工作,经过持续集成,我们的代码已经编译成Docker镜像,并被Push到私有仓库中。

在本节,我们接着前一小节的成功,讨论部署问题。

这里的部署指的是将微服务真正地运行在k8s集群系统中,主要涉及如下步骤:
* 获取可部署的Docker镜像版本
* 获取k8s集群操作权限
* 服务操作: 部署服务、重启服务等

其中获取k8s集群的权限有多种方式:
* 通过REST API
* 远程登录k8s的master节点

其中通过REST API的方式更加可靠、易于编程,但在k8s 1.7版本后,新增了权限控制,使用起来较为复杂。

在这里,我们采用直接登录k8s节点的方式。

## 获取Docker镜像的版本列表

前面已经提到,在持续集成时,会打包好最新的镜像到仓库中,并且制定版本为Jenkins的版本。

对于部署环节,却不一定总是需要上线最新版本的镜像。例如,我们上线了一个新功能,半小时后发现有个Bug,需要回滚,此时就需要上线前一个版本的镜像。

如何获取镜像对应的版本呢?我们可以通过"Active Choices Plugin"插件来实现。

首先用管理员登录,Manage Jenkins -> Manage Plugins,安装"Active Choices Plugin"插件。

随后新建一个项目,例如"lmsia-xyz-deploy",勾选"This project is parameterized",然后新增一个"Active Choices Parameter",进行如下配置:

![配置动态参数](./jenkins-docker-img-version.png)

其中的主要代码如下:
```groovy
// Variable
def proj_name="lmsia-xyz"
def curl_prefix="https://10.1.64.72/v2/$proj_name"

// Get From Docker Registry Using curl
import groovy.json.JsonSlurper
def cmd= "curl --insecure $curl_prefix/tags/list"
def object = new JsonSlurper().parseText(cmd.execute().text)

// Sort and return
return object.tags.sort { a, b -> b.compareToIgnoreCase a }
```

上述代码从Docker私有仓库获取"lmsia-xyz"这个镜像的所有版本,然后倒着排序后,返回给Jenkins插件。

点击底部保存,点击"lmsia-xyz-deploy"项目的"Build with Parameters",可以发现正常显示了版本列表:

![正常展示了版本列表](./jenkins-deploy-version.png)

## 新增远程主机

前面已经提到,我们将直接登录到k8s的主节点来执行部署命令。

为了实现这一点,首先要将该机器添加到Jenkins的SSH Site中。

用管理员帐号登录 -> Manage Jenkins -> Configure System, 找到SSH Site添加如下:

![配置远程执行主机](./jenkins-ssh-succ.png)

除了配置主机名、端口外,记得选择一个已经配置好的SSH私钥,这需要提前到Jenkins的Credentials中配置好。

接着我们回到lmsia-xyz-deploy项目,在Build环节新建一个"Execute script on remote host using ssh"

* SSH Site选择刚才配置好的主机
* Command里设置为"/home/coder4/deploy2k8s.sh $JOB_NAME $IMG_VERSION"

其中deploy2k8s.sh需要部署在这台远程的/home/coder4目录下(根据你的情况可自行更改),内容为:
```bash
#!/bin/bash
set -e

if [ x"$#" != x"2" ];then
    echo "Usage $0 proj_name img_version"
    exit -1
fi

# Const
DOCKER_REGISTRY="10.1.64.72"
PROJECT_NAME=$1
IMAGE_NAME=$(echo $PROJECT_NAME | sed -r 's/-deploy$//g')
IMAGE_VERSION=$2

# Generate yaml 
cat > ./deployment.yaml <<EOF

apiVersion: apps/v1
kind: Deployment
metadata:
  name: $IMAGE_NAME-deployment
spec:
  selector:
    matchLabels:
      app: $IMAGE_NAME 
  replicas: 2
  template:
    metadata:
      labels:
        app: $IMAGE_NAME
    spec:
      containers:
      - name: $IMAGE_NAME-ct
        image: $DOCKER_REGISTRY/$IMAGE_NAME:$IMAGE_VERSION
        ports:
        - containerPort: 8080
        - containerPort: 3000

EOF

# Deploy 
kubectl apply -f ./deployment.yaml

```

上述代码完成如下功能:
* 获取镜像名称和本次要上线的镜像版本
* 生成deployment模板
* 调用kubectl 部署

至于重启服务和停止服务,我们作为思考题,交给读者来实现。

至此,我们完成了持续部署的工作。

## 拓展与思考
1. 微服务的持续部署,除了部署更新版本,还需要停止或者重启,如何实现这一点,请结合本书介绍的内容,结合网上的其他资料,自行实现。
1. 上述模板只是简单的部署,如果需要实践前面章节介绍的微服务发现模型,需要进行哪些修改呢?


================================================
FILE: legacy/ms-delivery/ms-ci.md
================================================
# Jenkins持续集成

在本小节中,我们将讨论持续集成。

持续集成指的是: 频繁地(一天多次)将代码集成到主干。这里的集成不止是代码合并,还要保证可以通过编译、单元测试、集成测试。

持续集成的主要优点是:
* 快速发现错误。即所谓的"早发现、早治疗"。
* 减少开发、主分支之间的冲突。更频繁的分支合并,可以降低分支冲突发生的概率。
* 为持续部署打下基础。代码先要能集成起来,才能够进一步地部署。

针对我们当前的架构,持续集成分为如下几个步骤:
* 从Gerrit上check out代码
* Gradle编译项目
* 打包Docker镜像

## 部署Salve打包机

在持续构建的过程中,需要迁出代码、编译、打Docker镜像等步骤,如果都放在Jenkins的容器内执行,存在一些缺点:
* 影响Jenkins主服务性能。编译、打镜像都是非常耗费系统资源的操作。如果放到Jenkins上执行,势必会影响服务的流畅性和稳定性。
* 耦合过紧,不方便升级维护。如果要Jenkins支持打包、编译,需要自己定制镜像,即在Jenkins基础上安装Gradle、Java等工具。如果将来任何一个工具或Jenkins需要升级版本,就需要重新开发镜像,非常繁琐。

因此,通常都会新建一个独立于Jenkins的打包机,里面集成编译打包工具。Jenkins将打包机作为Slave添加到系统中,在打包时将调用Slave机器执行打包任务。

Slave打包机可以采用物理机、虚拟机或者容器,这里我们选择容器的方式,主要优点有:
* 故障恢复,方便运维。编译、打包的复杂程度较高,经常会导致打包机挂起,采用容器的方式,可以快速恢复故障。
* 版本可控,方便升级。持续集成的工具链需要逐步升级。例如Java、Gradle版本,每年都会更新几次,采用容器的方式,可以更好地管理版本,更精细地控制打包流程。
* 资源使用可控。编译、打包耗费较大资源,有时候为了保证整个系统稳定性,要限制打包使用的CPU、内存资源,使用容器技术可以轻松地做到这一点。
* 启动快速,方便扩展。随着持续集成的规模逐渐扩大,要同时执行多个任务,甚至要在打包高峰期,动态启动若干Slave,以提升并行度。这类似于微服务的副本扩展,容器集群(如k8s)对这种副本拓展有很好地支持。

我们首先来构建一个打包机的镜像,Dockerfile如下:
```shell
FROM ubuntu:18.04

# apt-add-repostory zip unzip git
RUN apt-get update
RUN apt-get install -y apt-utils software-properties-common zip unzip git

# SSH
RUN apt-get install -y openssh-server \
    && mkdir /var/run/sshd \
    && sed -ri 's/UsePAM yes/#UsePAM yes/g' /etc/ssh/sshd_config

# Java
ENV JAVA_HOME /usr/lib/jvm/java-8-oracle
RUN \
  echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | debconf-set-selections && \
  add-apt-repository -y ppa:webupd8team/java && \
  apt-get update && \
  apt-get install -y oracle-java8-installer

# Gradle
ENV GRADLE_HOME /opt/gradle
ENV GRADLE_VERSION 4.10
ARG GRADLE_DOWNLOAD_SHA256=248cfd92104ce12c5431ddb8309cf713fe58de8e330c63176543320022f59f18
RUN set -o errexit -o nounset \
	&& echo "Downloading Gradle" \
	&& wget --no-verbose --output-document=gradle.zip "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" \
	\
	&& echo "Checking download hash" \
	&& echo "${GRADLE_DOWNLOAD_SHA256} *gradle.zip" | sha256sum --check - \
	\
	&& echo "Installing Gradle" \
	&& unzip gradle.zip \
	&& rm gradle.zip \
	&& mv "gradle-${GRADLE_VERSION}" "${GRADLE_HOME}/" \
	&& ln --symbolic "${GRADLE_HOME}/bin/gradle" /usr/bin/gradle

# Create User
RUN useradd -m build 
RUN echo "build:build123" | chpasswd

# Docker ce
RUN apt-get install -y \
    apt-transport-https \
    ca-certificates \
    curl \
    software-properties-common
RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
RUN add-apt-repository \
    "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
    $(lsb_release -cs) \
    stable"
RUN apt-get install -y docker-ce
RUN usermod -aG docker build

# Clean
RUN apt-get remove -y apt-utils software-properties-common && \
    apt-get autoremove -y && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/cache/oracle-jdk8-installer

EXPOSE 22

# SSH Daemon
CMD ["/usr/sbin/sshd", "-D"]

```

如上所示,上述镜像主要完成了以下功能:
* JDK 8的安装
* SSHD服务的安装
* Gradle 4.10的安装
* build用户(密码build123)的配置
* Docker的安装和配置(这里是Docker inside Docker,我们只安装可执行文件,通过volulme映射使用物理机的docker)

有了上述镜像后,我们启动这个slave容器:
```shell
#!/bin/bash

# BUILD
docker build -t slave .

NAME="slave"

# submit to local docker node 
docker ps -q -a --filter "name=$NAME" | xargs -I {} docker rm -f {}
docker run \
    --name $NAME \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -p 22 \
    --detach \
    --restart always \
    -d slave:latest 

```

接下来,我们在Jenkins中添加这台slave机器。

使用管理员帐号登录,"Manage Jenkins" -> "Manage Nodes" -> "New Node",然后如下图所示配置:

![Jenkins配置Slave机器](./jenkins-slave.png)

主要的配置为:
* Remote root directory 家目录/home/build
* Usage 尽可能多使用(尽量不占用master进程)
* Launch method 使用ssh模式
 * Host slave即上面启动的容器
 * Credentials 可以新增一个密码方式的验证build/build123
* Availability 尽量让slave在线

配置成功默认是离线的,稍等一会,会提示"slave已经上线"。

## 持续集成第一步:迁出代码、编译

本节开篇已经提到,持续集成的第一步即从代码仓库中迁出代码,我们来完成这项工作。

首先在gerrit上准备一个项目,假设为lmsia-xyz,这是一个最简单的Spring Boot项目。

为了能够提交、迁出代码,需要将公钥配置到gerrit上,点击右上角的名字 -> Setting -> SSH Public Keys,填入即可完成。

准备好项目后,我们在Jenkins上新建一个"Freestyle"项目,命名为lmsia-xyz-build。

首先配置代码仓库,如下图所示:

![Jenkins配置Gerrit权限](./jenkins-gerrit.png)

* 在"Source Code Management"中,选择"Git",并填写gerrit的repo地址
* Credentials中新增一个用户,为gerrit中的用户,要填写私钥

此外,还要限制只能在slave上执行: Restrict where this project can be run中设置"slave"。

完成后点击底部的Save。

配置好后,我们执行第一次Build,在项目左侧菜单选择"Build Now",可以在Log中查看输出如下:
```
Building remotely on slave in workspace /home/build/workspace/lmsia-xyz-build
Cloning the remote Git repository
Cloning repository ssh://lihy@10.1.64.72:29418/lmsia-xyz
 > git init /home/build/workspace/lmsia-xyz-build # timeout=10
Fetching upstream changes from ssh://lihy@10.1.64.72:29418/lmsia-xyz
 > git --version # timeout=10
using GIT_SSH to set credentials 
 > git fetch --tags --progress ssh://lihy@10.1.64.72:29418/lmsia-xyz +refs/heads/*:refs/remotes/origin/*
 > git config remote.origin.url ssh://lihy@10.1.64.72:29418/lmsia-xyz # timeout=10
 > git config --add remote.origin.fetch +refs/heads/*:refs/remotes/origin/* # timeout=10
 > git config remote.origin.url ssh://lihy@10.1.64.72:29418/lmsia-xyz # timeout=10
Fetching upstream changes from ssh://lihy@10.1.64.72:29418/lmsia-xyz
using GIT_SSH to set credentials 
 > git fetch --tags --progress ssh://lihy@10.1.64.72:29418/lmsia-xyz +refs/heads/*:refs/remotes/origin/*
 > git rev-parse refs/remotes/origin/master^{commit} # timeout=10
 > git rev-parse refs/remotes/origin/origin/master^{commit} # timeout=10
Checking out Revision eab8a79ff6cde375c017b6f9eec29dff02a0bb85 (refs/remotes/origin/master)
 > git config core.sparsecheckout # timeout=10
 > git checkout -f eab8a79ff6cde375c017b6f9eec29dff02a0bb85
Commit message: "MOD: init commit"
First time build. Skipping changelog.
Finished: SUCCESS
```

如上所示,我们成功地从代码仓库迁出了代码,第一步顺利完成!

在迁出代码后,我们需要进行编译,回到lmsia-xyz-build项目的配置中,找到Build选项,新增一个"Execute shell"步骤,命令输入"gradle build",点击底部"Save"。

再次执行"Build Now",发现项目依然执行成功,查看日志,可以发现编译也成功地执行了!

至此,我们已经完成了代码的迁出和编译。

## 打包镜像并上出到私有仓库

在lmsia-xyz-build项目中新建一个Shell步骤,内容如下:
```
$HOME/ms2docker.sh
```

上述脚本需要添加到slave的镜像中,直接COPY即可,这里不再赘述。

脚本内容如下:
```shell
#!/bin/bash
set -e

# Const
DOCKER_REGISTRY="10.1.64.72"
PROJECT_VERSION=${BUILD_NUMBER:-1}
PROJECT_NAME=$(basename `pwd`| sed -r 's/-build$//g')
SERVER_NAME="$PROJECT_NAME-server"
JAR_NAME="$SERVER_NAME.jar"
DOCKER_FULLNAME="$PROJECT_NAME:$PROJECT_VERSION"

# Copy Jar
find . -name "$SERVER_NAME*.jar" -exec cp {} ./$JAR_NAME \;

# Generate Dockerfile
cat > ./Dockerfile <<EOF
FROM anapsix/alpine-java:8_server-jre

VOLUME /tmp /app
WORKDIR /app
EXPOSE 8080 3000
COPY ${JAR_NAME} /app
CMD ["java", "-jar", "/app/${JAR_NAME}"]

EOF

# Build
docker build .
docker build -t $PROJECT_NAME .
docker tag $PROJECT_NAME $DOCKER_REGISTRY/$DOCKER_FULLNAME
docker push $DOCKER_REGISTRY/$DOCKER_FULLNAME

```

简单解释一下:
* 首先获得项目名称(根据当前执行的工作文件夹)
* 查找生成的server.jar,并拷贝到当前目录
* 编译一个Docker的镜像,包含Java环境和server的jar包
* 上传Docker镜像到私有仓库

在Jenkins配置好后,点击保存,重新打包。

看一下结果:
```
....
The push refers to repository [10.1.64.72/lmsia-xyz]
ce4c6e84ae9a: Preparing
c24b758e34d0: Preparing
c28e906f67c9: Preparing
cd7100a72410: Preparing
c28e906f67c9: Layer already exists
c24b758e34d0: Layer already exists
cd7100a72410: Layer already exists
ce4c6e84ae9a: Pushed
6: digest: sha256:86ccfb07945bdaf61c90470e3302774cde31d41b6c1a26647ea92fdb681536b3 size: 1158
Finished: SUCCESS
```

如上所述,已经成功地打好Docker镜像并上传到私有仓库中。

至此,我们经完成了持续集成的所有步骤,它包含:
* 从gerrit上迁出代码到本地(slave机器)
* 调用gradle编译
* 使用Docker打镜像并上传到私有仓库

## 拓展与思考
1. 在开发过程中,微服务会频繁的打包、持续集成。这会产生大量历史镜像文件,这些历史版本并不会被使用,却浪费了大量的磁盘空间。请自行查找资料,实现"只保留最近3个最新项目镜像"这一功能。
1. 本节的"持续集成",主要指项目的编译部分。然而,在实际项目中,还需要在集成阶段进行单元测试、集成测试。如何在"持续集成"阶段完成测试工作,请结合实际情况思考这一问题。


================================================
FILE: legacy/ms-discovery/README.md
================================================
# 微服务的自动发现与负载均衡 

采用微服务架构后,巨无霸服务被拆分为若干逻辑独立的微服务,导致服务数量逐渐上升。此外,为了保证系统的高可用和高性能,每一个微服务都会运行若干副本,这更进一步地导致微服务运行实例数量的攀升。

面对数量逐渐攀升的微服务实例,我们不可能将服务的IP和端口都写在配置文件中,这种方案会使得系统无法维护。因此,采用微服务架构后,微服务服务(及实例)的自动发现是首先要解决的问题。

前面已经提到,出于高可用和高性能考量,微服务往往同时部署多个副本。如何实现自动负载均衡呢?这是本章要讨论的第二个重点。

本章将从容器技术谈起,介绍Docker和Kubernetes的优势。随后,我们会通过几个小例子,让大家快速上手Kubernetes。最后,我们结合一个微服务的实例,探讨如何利用Kubernetes的Service实现服务的自动发现与负载均衡。


================================================
FILE: legacy/ms-discovery/msd.md
================================================
# 微服务的自动发现

在熟悉了的基本操作后,我们来讨论下如何实现微服务的自动发现。

Service是在Pod基础上做的另一层抽象,通过虚拟IP的方式,提供了统一的代理入口和负载均衡。

Service本身不会创建Pod,而是通过标签的方式与已有Pod产生关联,这与Deployment是类似的。因此,在创建第一个Service前,我们需要先应用之前的lmsia-abc-server-deployment,具体可参考前一节[Kubernetes 快速入门](kus-intro.md)

下面来看一下Service描述文件,lmsia-abc-server-service.yaml
```yaml
apiVersion: v1
kind: Service
metadata:
  name: lmsia-abc-server-service
spec:
  selector:
    app: lmsia-abc-server 
  ports:
  - name: http
    protocol: TCP
    port: 8080
  - name: rpc 
    protocol: TCP
    port: 3000
```

与Deployment相比,上述Service的描述文件更简单一些。
 * kind: 类型是Service
 * metadata.name: 定义了Service名字
 * spec.selector.app: 定义了要关联的Pod标签
 * spec.ports: 定义了需要进行负载均衡的端口,这里定义了两套需要负载均衡的端口,http的8080和rpc的3000。

有了描述文件后,我们来应用服务:
```shell
kubectl apply -f lmsia-abc-server-service.yaml

service "lmsia-abc-server-service" created
```

成功创建Service后,可以使用'describe service'来查看:
```
kubectl describe service lmsia-abc-server-service

Name:              lmsia-abc-server-service
Namespace:         default
Labels:            <none>
Annotations:       kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"lmsia-abc-server-service","namespace":"default"},"spec":{"ports":[{"name":"htt...
Selector:          app=lmsia-abc-server
Type:              ClusterIP
IP:                10.109.20.138
Port:              http  8080/TCP
TargetPort:        8080/TCP
Endpoints:         172.17.0.4:8080,172.17.0.5:8080
Session Affinity:  None
Events:            <none>

```

上面返回的结果中,有一些关键信息:
 * Type: 指的是ServiceType,ClusterIP是仅供集群内访问的负载均衡IP。类似的,如果想将虚拟IP暴露给集群外,可以使用NodePort等,具体可以参考官方文档[Publising Service Types](https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types)。
 * IP: 服务提同的虚拟IP地址。
 * Port: 微服务进程上的端口,即HTTP的8080和RPC的3000
 * TargetPort: 虚拟IP对外提供负载均衡的端口,由于我们未单独制定,默认是与上述Port保持一致的。
 * Endpoints:我们在Deployment中定义的两个Pod。Service通过虚拟IP将流量分发到这两个后端Pod上。

让我们来验证下负载均衡的配置是否生效,由于rpc接口的数据格式较为复杂,在此我们仅验证http端口。

首先登录到minikube
```shell
minikube ssh

curl http://10.109.20.138:8080/lmsia-abc/api/
Hello, REST

curl http://10.109.20.138:8080/lmsia-abc/api/
Hello, REST

```

我们执行了两次,都成功了,那么这个请求真的被均匀地分发到后端的进程上了么?我们需要验证一下。

首先获取两个容器的ID
```shell

# list pod
kubectl get pods -l app=lmsia-abc-server
NAME                                          READY     STATUS    RESTARTS   AGE
lmsia-abc-server-deployment-bd4949ff9-7bgvq   1/1       Running   0          16m
lmsia-abc-server-deployment-bd4949ff9-mlmlq   1/1       Running   0          16m

# get container id for pod1
kubectl describe pod lmsia-abc-server-deployment-bd4949ff9-7bgvq
...
Name:           lmsia-abc-server-deployment-bd4949ff9-7bgvq
...
Containers:
  lmsia-abc-server-ct:
    Container ID:   docker://a146ee545d11638a331d1696e7e6e3c88cc3231b97f3eb50c63cb9f50724cf2c
...

# get container id for pod 2
kubectl describe pod
...
Name:           lmsia-abc-server-deployment-bd4949ff9-mlmlq
...
Containers:
  lmsia-abc-server-ct:
    Container ID:   docker://608decbb198dcbdce5442a4401eeeec1cb316e483ddba2d5c993ea10081a5e6a
...

```

登录minikube集群,分别查看两个Container的日志
```shell
minikube ssh

# check pod 1 access log
$ docker exec -i -t a146ee545d11638a331d1696e7e6e3c88cc3231b97f3eb50c63cb9f50724cf2c cat /app/logs/access_log.2018-05-14.log
10.0.2.15 - - [14/May/2018:07:27:57 +0000] "GET /lmsia-abc/api/ HTTP/1.1" 200 11

# check pod 2 access log
$ docker exec -i -t 608decbb198dcbdce5442a4401eeeec1cb316e483ddba2d5c993ea10081a5e6a cat /app/logs/access_log.2018-05-14.log
10.0.2.15 - - [14/May/2018:07:27:56 +0000] "GET /lmsia-abc/api/ HTTP/1.1" 200 11
```

这里需要说明下'docker exec -i -t',是针对Docker容器执行命令,要执行的命令即后面的cat /app/logs....

查看了两个Pod对应的Container的日志,可以发现:虽然我们的curl是访问的虚拟IP,但是流量被均衡地分发到了2个后端容器上。至此,我们已经通过Service实现了多节点的自动负载均衡。

需要指出的是:Kubernetes的虚拟IP内置了多种实现,目前以ipvs性能最好,具体可以查看[Virtual IPs and service proxies](https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies)

现在让我们来回顾下这一节的标题:"微服务的自动发现"。对于服务发现这个需求,我们目前的效果似乎并不这么完美,为什么这样说呢?我们目前是通过虚拟IP直接访问的服务,但在实际生产环境中,每个Service创建的虚拟IP并不固定,我们不可能将这些虚拟IP分别配置在依赖的众多微服务中。

幸运的是,Kubernetes早就为我们解决了这个问题。在创建Service的同时,Kubernetes还为我们创建了一条DNS记录,我们可以通过域名直接访问虚拟IP:
```shell
docker exec -i -t 608decbb198dcbdce5442a4401eeeec1cb316e483ddba2d5c993ea10081a5e6a busybox wget -q -O - http://lmsia-abc-server-service:8080/lmsia-abc/api/

Hello, REST
```

如上所示,通过lmsia-abc-server-service这个域名,就可以成功地访问虚拟IP了。对于ClusterIP的Service,域名的默认组成是'服务名.服务所在命名空间.svc.cluster.集群域名',或者简单使用`服务名`[^1],上面例子中我们采用的就是后者。

让我们用一张图来回顾下服务发现、负载均衡流程:

![基于Kubernetes的服务发现与负载均衡](./service-discovery.png "基于Kubernetes的服务发现与负载均衡")

如上图所示:
1. 约定好微服务Service的命名方式
1. 通过DNS服务获取微服务Service对应的虚拟IP(VIP)
1. 访问VIP和端口(3000)
1. Kubernetes的VIP自动完成了负载均衡,转发到后端Service B的3个节点(Pod/Docker)上

至此,我们借助Kubernetes的Service功能,"近似完美"地实现了服务的注册与发现。

为什么讲"近似完美"呢?这里还会有一个小坑。熟悉DNS协议的朋友知道,为了提升查询效率,DNS被设计成可以多级缓存的。在Java的JVM虚拟机上,也会进行DNS缓存,但这个缓存有效期默认是-1即永久。这也就意味着,如果我们删除这个Service重新创建,那么虚拟IP的变更将不会自动反馈到相应微服务的JVM中。

为了解决这个小坑,一般建议修改JVM的安全设置,修改缓存TTL时间,具体可以参考[亚马逊AWS的这篇介绍](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/java-dg-jvm-ttl.html)。

我们为本章构建的Docker镜像也自动解决了这个问题:

```shell
FROM anapsix/alpine-java:8_server-jre

WORKDIR /app

RUN mkdir -p /app/logs

ADD lmsia-abc-server.jar /app

CMD ["java", "-jar", "lmsia-abc-server.jar"]

```

其中`anapsix/alpine-java:8_server-jre`是我们依赖的基础镜像,它将DNS Cache设置为了10秒钟,读者也可以直接使用这个基础镜像。

需要特别说明的时:若想使用上述的自动发现机制,必须使用Kubernetes的DNS服务,它默认是开启的:
```shell
kubectl -n kube-system get svc kube-dns

NAME       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)         AGE
kube-dns   ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP   3d
```

通过Kubernetes创建的Pod(Docker),已经自动配置了上述DNS。若想在在集群外使用这个DNS,有两种方案:
* 将DNS通过NodePort的方式暴露出去,可以参考[这篇讨论](https://stackoverflow.com/questions/37449121/how-to-expose-kube-dns-service-for-queries-outside-cluster)
* 打通办公内网和集群内网,本书后续章节[OpenVPN + NAT 打通办公网与IDC](devops/openvpn-nat.md)将对此做出介绍。

[^1]: 这一特性并未记录在官方文档中,本书假设该特性持续有效。


================================================
FILE: legacy/ms-discovery/service-discovery.xml
================================================
<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>

================================================
FILE: legacy/ms-log/README.md
================================================
# 微服务日志监控

与前端或移动端产品不同,微服务运行于后台,我们不能直观的观察到服务端的运行状况。因此,合理地记录日志是检查服务端运行状态,查找问题的有效手段。

服务端日志的常见用途有:
* 业务日志:对一些分支条件进行简单记录,方便日后排查。
* 异常记录:记录运行时异常或业务逻辑异常,方便事后排查。
* 性能定位:日志系统一般自带时间戳,可以通过此方法排查函数调用的时间消耗。

对于微服务架构,记录下来日志只是第一步,如何使用日志是更大的难点。

试想在微服务的分布式系统下,有多种微服务,每个微服务又存在多个副本,日志文件可能散落在几百个不同的路径下。

这种情况下,面临着如下挑战:
* 如何快速找到某个微服务的所有(或某个)副本的日志?
* 微服务之间存在调用,如何从一次完整的调用角度,来分析相关服务的日志?
* 如何对异常日志进行预警?

上述挑战实际对应了日志监控的三个问题,即
* 微服务日志的收集、管理与查询
* 微服务的调用链跟踪
* 微服务异常日志的预警

本章将从微服务的日志系统展开讨论,探讨了使用Logback记录微服务日志的相关问题。

接着,我们讨论如何实现调用链的跟踪,并引入了TraceId类库。

最后,讨论如何使用"EBLK架构"对微服务的日志进行收集、管理、查询。



================================================
FILE: legacy/ms-log/elk-devops.md
================================================
# ELK日志分析平台的运维

在上一节中,我们在日志文件中增加了调用链信息,方便我们追踪每一次调用的完整关系链条。

尽管有了追踪信息,可以更好地排查信息。但在微服务架构下,微服务众多,每个微服务又会启动若干个副本,日志文件的数量会随着文件系统迅速增加。

为了排查一个问题,我们可能要分别到十几个服务上打开几十个不同的文件,效率非常低下。

ELK就是在这种场景下营运而生的,ELK是一套数据分析套件,由Elasticsearch, Logstach, Kibana组成。在微服务架构的应用场景下,一般用来分析日志。

在ELK套件中:
* Logstash负责从不同的微服务、不同的副本上收集日志文件,进行格式化。
* Elasticsearch负责日志数据的存储、索引
* Kibana提供了友好的数据可视化、分析界面

![ELK套件流程图](./elk-stack.jpg "ELK套件流程图")

在本节中,我们暂不接入微服务的日志,单纯探讨ELK套件的运维工作。

与之前类似,我们的ELK套件将运行在Kubernetes集群上。

## Elasticsearch的运维

Elasticsearch是ELK套件的核心与中枢。我们首先来看一下它的运维工作。

Elasticsearch的索引需要持久化存储,我们首先声明Pv:

```yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv031 
spec:
  storageClassName: standard
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 20Gi
  hostPath:
    path: /data/pv031/

```

然后创建一下这个pv:
```shell

kubectl apply -f ./pvs.yaml

```

下面看一下elasticserch的定义:

```yaml

apiVersion: v1
kind: Service
metadata:
  name: es
spec:
  ports:
  - name: p2
    port: 9200
  - name: p3
    port: 9300
  selector:
    app: elasticsearch
  clusterIP: None
---
apiVersion: apps/v1
kind: StatefulSet 
metadata:
  name: elasticsearch
spec:
  selector:
    matchLabels:
      app: elasticsearch
  serviceName: "es"
  replicas: 1
  template:
    metadata:
      labels:
        app: elasticsearch 
    spec:
      hostname: elasticsearch
      containers:
      - name: elasticsearch-ct
        image: docker.elastic.co/elasticsearch/elasticsearch:6.3.1 
        ports:
        - containerPort: 9200 
        - containerPort: 9300
        env:
        - name: "ES_JAVA_OPTS"
          value: "-Xms384m -Xmx384m"
        volumeMounts:
        - mountPath: /usr/share/elasticsearch/data 
          name: elasticsearch-pvc
  volumeClaimTemplates:
  - metadata:
      name: elasticsearch-pvc
    spec:
      storageClassName: standard
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 20Gi

```

如上所述:
* 考虑到日志数据量大了之后,可能需要分片,我们这里采用了StatefulSet,但目前只有一台服务器。
* 暴露两个端口9200和9300,前者是Restful接口,后者是集群同步接口
* 采用IP直发,service伪组名是"es"。这样配置后,所有Pod都可以通过elasticsearch-0.es来直接访问这台服务器

启动一下:
```yaml
kubctl apply -f ./elasticsearch.yaml
```

如果启动失败,可以查看日志,可能是如下原因:
```
kubectl logs elasticsearch-0

...
vm.max_map_count < 262144
...

```

这种情况,可以使用具有sudo权限的帐号,更改宿主机(物理机)的配置:
```yaml
sudo sysctl -w vm.max_map_count=262144
```

再次启动一下,可以发现启动成功:
```yaml
NAME                                                READY     STATUS    RESTARTS   AGE
elasticsearch-0                                     1/1       Running   4          6h
```

## Logstash运维

在启动了Elasticsearch后,我们来看一下Logstash的运维。

前面已经提到了,我们本节先不会接入Spring Boot的日志,为了方便演示,我们先Mock一个定时任务,每间隔5秒生成日志:
```yaml
apiVersion: v1
data:
  logstash.yml: |
    http.host: "0.0.0.0"
    xpack.monitoring.elasticsearch.url: http://elasticsearch:9200
    input {
      heartbeat {
        interval => 5
        message  => 'Hello from Logstash 💓'
      }
    }
    
    output {
      elasticsearch {
        hosts    => [ 'elasticsearch-0.es' ]
        user     => 'elastic'
        password => ''
      }
    }

kind: ConfigMap
metadata:
  name: logstash-configmap

```

上述是一个ConfigMap,我们在本书中是第一次使用它。它相当于一个可以加载的Volume,可以方便的直接追加到Pod上。

来创建这个ConfigMap:
```yaml
kubectl apply -f logstash-configmap.yaml
```

下面看一下Logstash的部署:
```yaml
apiVersion: v1
kind: Service
metadata:
  name: ls
spec:
  ports:
  - name: p
    port: 5000
  selector:
    app: logstash
  clusterIP: None
---
apiVersion: apps/v1
kind: StatefulSet 
metadata:
  name: logstash
spec:
  selector:
    matchLabels:
      app: logstash
  serviceName: "ls"
  replicas: 1
  template:
    metadata:
      labels:
        app: logstash
    spec:
      hostname: logstash
      containers:
      - name: logstash-ct
        image: docker.elastic.co/logstash/logstash:6.3.1 
        ports:
        - containerPort: 5000 
        env:
        - name: "ES_JAVA_OPTS"
          value: "-Xms384m -Xmx384m"
        - name: "XPACK_MONITORING_ENABLED"
          value: "false"
        - name: "XPACK_MONITORING_ELASTICSEARCH_URL"
          value: "http://elasticsearch-0.es:9200"
        volumeMounts:
        - name: logstash-configmap
          mountPath: /usr/share/logstash/pipeline/logstash.conf
          subPath: logstash.conf
      volumes:
      - name: logstash-configmap
        configMap:
          name: logstash-configmap

```

如上所述:
* 我们使用了刚才配置的logstash-configmap,并覆盖到Pod的/usr/share/logstash/pipeline/logstash.conf,这个文件中
* 监控地址是elasticsearch-0.es:9200,即前面启动的es服务地址

启动一下:
```shell
kubectl apply -f ./logstash.yaml
```

稍等一会,启动成功:
```shell

NAME                                                READY     STATUS    RESTARTS   AGE
logstash-0                                          1/1       Running   0          7h

```

## Kibana的运维

最后,我们来看一下Kibana的运维:
```yaml

apiVersion: v1
kind: Service
metadata:
  name: kb
spec:
  selector:
    app: kibana 
  clusterIP: None
---
apiVersion: apps/v1
kind: Deployment 
metadata:
  name: kibana 
spec:
  selector:
    matchLabels:
      app: kibana 
  replicas: 1
  template:
    metadata:
      labels:
        app: kibana 
    spec:
      hostname: kibana
      containers:
      - name: kibana-ct
        image: docker.elastic.co/kibana/kibana:6.3.1 
        ports:
        - containerPort: 5601
          hostPort: 5601
        env:
        - name: "ES_JAVA_OPTS"
          value: "-Xms384m -Xmx384m"
        - name: "ELASTICSEARCH_URL"
          value: "http://elasticsearch-0.es:9200"
        - name: "XPACK_MONITORING_ENABLED"
          value: "false"
```

一般来说,Kibana作为前端展示组件,只需要一台就够了,我们直接用了Deployment。

尝试打开浏览器访问一下:

![Kibana界面图](./kibana-chrome.png "Kibana界面图")

如果一切顺利,可以发现,访问成功。

对于新一些的ElasticSearch/Kibana版本,可能需要先配置一下索引,比较简单,跟着向导就可以完成。

Kibana的功能非常强大,拿来做日志分析实际有点大材小用。感兴趣的话,可以参考[官方使用教程](https://www.elastic.co/guide/en/kibana/current/getting-started.html)。

我们对ELK的运维就介绍到这里。


================================================
FILE: legacy/ms-log/sb-eblk.md
================================================
# Spring Boot整合EBLK日志分析平台

不知道你有没有注意到,这一节的标题是"EBLK日志分析平台",而上一节的标题中是"ELK日志分析平台"。是的,你没有看错,这也不是笔误。

在ELK平台中,Logstash负责收集微服务的各种Log文件,发送给ElasticSearch。

当微服务数量少、副本数也不多的时候,Logstash是可以胜任的。随着微服务数量不断增多,副本数不断增长,Logstash的负载会越来越高,极易造成单点故障。

此外,在我们的微服务架构下,各个微服务进程是运行在Kubernetes集群上的,它们的日志文件可能分散在各个物理机上。如何让单一的Logstash收集这些遍布各处的日志文件,也是一个难题。

一个简单的想法就是启用边车模式,即每个微服务启动时,同时伴随部署一个Logstash,这样就可以解决单点故障和收集的问题。

想法是好的,但Logstash本身的结构较为复杂,同时具有监听文件、网络、批处理等各种复杂功能,此外Logstash需要JVM运行环境,内存占用较大。

为了更加轻量级级的收集日志,ElasticSearch推出了Beat,我们以边车模式伴随微服务进行部署。关于Beat与Logstash的对比,可以参考[这篇文章](https://logz.io/blog/filebeat-vs-logstash/)。

Beat负责收集日志,并将日志发送给Logstash。这样看起来还是没有解决Logstash的单点故障?

是的,但经过Beat转发后,我们实际上可以配置多个Logstah结点从而解决掉单点故障。

此外,Beat可以缓存日志,当Logstash挂掉后,会自动重试。Logstash恢复后,可以继续处理日志的发送。加上B这一层后,整EBLK的架构如下所示:

![EBLK架构图](./eblk.png "EBLK架构图")

在本小节的前半部分,我们将在一个受限环境中,使用Beat收集日志,并发送给Logstash。后半部分,将讨论如何在Kubernetes中应用便车模式,让Beat伴随微服务一同启动。

## 使用Beat收集日志

Beats是一系列日志收集工具的统称,官方推出了多种Beat,如:Filebeat, Metricbeat, Packetbeat, Winlogbeat等等,详细可以参见[官方介绍](https://www.elastic.co/products/beats)。

在我们的场景下,需要解析微服务输出的日志文件,直接用Filebeat即可。

首先来看一下FileBeat的配置:

```yaml

apiVersion: v1
data:
  filebeat.yml: |
    filebeat.inputs:
    - type: log
      enabled: true
      multiline.pattern: '^2'
      multiline.negate: true
      multiline.match: after
      name: filebeat-test
      paths:
        - /usr/share/filebeat/*.log
    output.logstash:
      hosts: ["logstash-0.ls:5555"]

kind: ConfigMap
metadata:
  name: filebeat-configmap

```

上述配置包含2个部分:
* 输入监听/user/share/filebeat/下后缀为log的文件,这里只是限定环境下的测试,并非线上微服务的日志,支持多行自动合并为同一个事件(主要是异常时调用堆栈信息)。
* 输出到logstash, logstash-0.ls:5555

然后我们看一下FileBeat的服务定义:
```yaml
apiVersion: v1
kind: Service
metadata:
  name: ls
spec:
  ports:
  - name: p
    port: 5000
  selector:
    app: filebeat
  clusterIP: None
---
apiVersion: apps/v1
kind: StatefulSet 
metadata:
  name: filebeat
spec:
  selector:
    matchLabels:
      app: filebeat
  serviceName: "ls"
  replicas: 1
  template:
    metadata:
      labels:
        app: filebeat
    spec:
      hostname: filebeat
      containers:
      - name: filebeat-ct
        image: docker.elastic.co/beats/filebeat:6.3.2 
        env:
        - name: "ES_JAVA_OPTS"
          value: "-Xms384m -Xmx384m"
        - name: "XPACK_MONITORING_ENABLED"
          value: "false"
        - name: "XPACK_MONITORING_ELASTICSEARCH_URL"
          value: "http://elasticsearch-0.es:9200"
        volumeMounts:
        - name: filebeat-configmap
          mountPath: /usr/share/filebeat/filebeat.yml
          subPath: filebeat.yml
      volumes:
      - name: filebeat-configmap
        configMap:
          name: filebeat-configmap

```

如上所示,配置基本与之前的Logstash相同,并且加载了刚配置好的filebeat.yml。

## Logstash汇总日志

对应地,logstash也需要做对应的调整:
```yaml

apiVersion: v1
data:
  logstash.conf: |

    input {
      beats {
        port => 5555
      }
    }

    filter {
      grok {
        match => {"message" => "(?m)^(?<TIMESTAMP>%{YEAR}-%{MONTHNUM}-%{MONTHDAY} %{TIME}) \[%{LOGLEVEL:LEVEL}\] \[(?<THREAD>.*?)\] \[(?<LOGGER>.*?)\] \[tr=(?<TRACE_ID>.*?)\]\s+(?<MSG>.*)" }
      }
    }
    
    output {
      elasticsearch {
        hosts    => [ 'elasticsearch-0.es' ]
        user     => 'elastic'
        password => ''
      }
    }

kind: ConfigMap
metadata:
  name: logstash-configmap

```

如上所示,我们修改了logstash的配置:
* input是beat格式,端口5555,与上面filebeat的配置对应
* filter对输入的beat事件进行解析。这里使用了grok插件,具体的语法可以参考[官方grok插件介绍](https://www.elastic.co/guide/en/logstash/current/plugins-filters-grok.html)
* output输出到elasticsearch,这里没有变化

我们重启Logstash和FileBeat后,尝试向FileBeat的Docker中写入几行日志,稍等几秒,打开Kibana,可以发现,日志已经可以检索到了。

![接入了FileBeat后的Kibana](./kibana-filebeat.png "接入了FileBeat后的Kibana")

## 将FileBeat与Spring Boot进行整合

前面已经提到,微服务数量、副本数众多、遍布在集群的各个物理机上,日志收集、汇总起来非常麻烦,所以一般来说,需要使用边车模式,即一个微服务伴随一个日志收集器(FileBeat)。

上述模式的实现,有两个技术选择:
* 使用Kubernetes的Pod多容器模式
* 手动将FileBeat打包进微服务的镜像内。

方案二比较传统,也易于理解,可以参考[这篇文章](https://stackoverflow.com/questions/47811121/dockerfile-springboot-app-with-filebeat)

而方案一,则是利用了Kubernetes的原生支持特性。

Kubernets中的最小操作单位是Pod,Pod中可以启动多个Docker容器,且同他们之间共享同样的磁盘、端口。

关于微服务的服务定义、FileBeat定义,我们前面已经分别介绍过了,所需要做的,就是将他们“粘贴”到同一个Pod里面。

这里,我不再赘述具体描述,而是作为一个思考题留给你来实现。如果实现起来有困难,可以参考[Multi-Container Pods in Kubernetes](https://linchpiner.github.io/k8s-multi-container-pods.html)。


================================================
FILE: legacy/ms-log/sb-logback.md
================================================
# Spring Boot配置Logback及HTTP日志

系统上先后,需要进行一系列的运维、监控工作,可能还需要排查业务故障和系统问题。

服务已经上线了,不能像本地开发的一样“”打断点调试“,此时,日志的作用就非常重要了。

与其他服务框架类似,Spring Boot也默认集成了日志系统,默认采用Logback日志类库。

Logback是Log4j的作者开发的另一款日志类库,与其他同类竞品相比,它的优势有:
* 更高的性能,官方说比Log4j快10倍以上
* 原生兼容slf4j
* 支持多环境配置、自动切换、压缩等高级功能

在本节的前半部分,我们将讨论如何在Spring Boot中如何使用Logback。本节的后半部分,我们看一下如何在Spring Boot中启用HTTP访问日志(内嵌的Tomcat日志)。

## Spring Boot中配置Logback

在Spring Boot中配置Logback只需要两步:
* 确认类路中含有logback,这一般是通过其他starter自动带上的,例如spring-boot-starter-web
* 定义配置文件:logback-spring.xml

我们来看一下配置好的文件:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <!-- For console -->
    <appender name="ConsoleAppender" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <charset>UTF-8</charset>
            <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] [%thread] [%logger] [tr=%mdc{TRACE_ID:-0}] %msg %n</Pattern>
        </encoder>
    </appender>

    <!-- For file with daily rollover -->
    <appender name="ServerFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <charset>UTF-8</charset>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] [%thread] [%logger] [tr=%X{TRACE_ID:-0}] %msg %n</pattern>
        </encoder>

        <file>/app/logs/lmsia-abc.log</file>

        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- daily rollover with gz -->
            <fileNamePattern>/app/logs/lmsia-abc.%d{yyyy_MM_dd}.log.gz</fileNamePattern>
            <!-- keep 30 days' max -->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
    </appender>

    <!-- console only if local active -->
    <springProfile name="local">
        <root level="INFO">
            <appender-ref ref="ConsoleAppender"/>
        </root>
    </springProfile>
    <!-- file only if test or online active -->
    <springProfile name="test,online">
        <root level="INFO">
            <appender-ref ref="ServerFileAppender"/>
        </root>
    </springProfile>

</configuration>

```

如上所示,我们的配置中包含了2个Appender(可理解为两种日志输出方法):
* ConsoleAppender: 直接输出到命令行
* ServerFileAppender: 输出到/app/logs/lmsia-abc.log文件中,并且:按天自动切换文件、并做gz压缩、最多保留30天。

上述切换、压缩、30天仅通过几行就搞定了,可见logback的强大之处!

在下面,我们通过对不同profile的判断,可以让不同的Appender生效。
当我们在本地执行时,默认是local的profile,此时我们只运行ConsoleAppender,即直接输出到命令行,方便调试。
当在服务器执行时,如测试环境test和生产环境online,我们只启用ServerFileAppender。因为此时没有人会看stdout的输出,都是通过看文件的方式来看日志的。

最后,我们简单看一下日志格式,即Pattern:
```
%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] [%thread] [%logger] [tr=%X{TRACE_ID:-0}] %msg %n
```

几个部分分别表示:
* 日期,如2018-06-07 18:30:23.124
* 日志级别,INFO, ERROR等
* 日志线程,如果有名字会优先用名字,没有用线程ID
* Logger名字,一般是类名
* TraceId,追踪信息,下一节将介绍它
* 消息体及换行,如果有异常及异常栈,会自动输出在后面

在代码中使用,也是非常简单:

```
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

private Logger LOG = LoggerFactory.getLogger(getClass());

LOG.info("Test");
```

## 配置Tomcat日志

Spring Boot默认内置了Tomcat服务器,从而实现了真正的"开箱即用",如何开启Tomcat的HTTP访问日志呢?

一般有两种方法:
* 在yaml中配置
* 通过代码实现

其中yaml中配置的方案最简单,但每个项目都要配置一次,非常麻烦,网上资料很多,这里不做介绍了。

我们重点看一下第二种方案,我们可以将它抽成一个包,别的项目引用这个包时候,自动启用HTTP访问日志。

```java
import org.apache.catalina.valves.AccessLogValve;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
@ConditionalOnWebApplication
public class TomcatAccessLogConfiguration
        extends WebMvcConfigurerAdapter implements EmbeddedServletContainerCustomizer {

    private Logger LOG = LoggerFactory.getLogger(getClass());

    @Override
    public void customize(ConfigurableEmbeddedServletContainer container) {
        if (container instanceof TomcatEmbeddedServletContainerFactory) {
            TomcatEmbeddedServletContainerFactory factory = (TomcatEmbeddedServletContainerFactory) container;
            AccessLogValve accessLogValve = new AccessLogValve();
            accessLogValve.setEnabled(true);
            accessLogValve.setDirectory("/app/logs/");
            accessLogValve.setPattern("common");
            accessLogValve.setSuffix(".log");
            factory.addContextValves(accessLogValve);
        } else {
            LOG.error("This customizer does not support your configured container!");
        }
    }
}

```

如上所示:
* 当启用Web时,自动激活这个自动配置
* 使用默认的日志格式common
* 路径在/app/logs下,后缀是.log

当然不要忘记加一个spring.factories
```
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.coder4.lmsia.commons.http.configuration.TomcatAccessLogConfiguration
```

这样,当别的Spring Boot项目引用这个库时,就会自动启用HTTP日志了。

这个HTTP日志也是支持按天滚动,只不过不支持压缩,如果你想对其进行更多定制,推荐直接阅读Tomcat的相关源代码。


================================================
FILE: legacy/ms-log/sb-trace.md
================================================
# Spring Boot整合分布式调用链追踪

在上一节,我们讨论了如何在Spring Boot项目中配置LogBack日志系统。

如果是传统的巨服务架构,有日志就能够满足基本的需求了。

但面对微服务,事情变得有一些复杂:
* 微服务之间存在复杂的调用链路,例如A -> B -> C
* 为了高可用,每个微服务可能存在多个实例

设想我们有A, B, C三个微服务,每个微服务有2个实例,在调用链A -> B -> C的过程中,发生了异常,导致某个请求挂掉了。

此时,我们已经有日志系统了,该如何检查呢?我们需要一次检查2个A服务,如果运气不好的话,可能没有异常,我们接下来检查B服务,也可能没有异常,最后检查C服务,发现了异常。

在上述任务排查过程中不难看出,在微服务架构下,各个服务的相互调用非常复杂。

实际上,我们可以引入调用链的追踪机制,来查明这种关系。

调用链追踪是这样一直机制:对于每一次调用,例如从A开始,就生成一条"调用链路"并赋一个追踪信息(后简称TraceId),调用到B时,会继承这个TraceId,如果它又调用了C服务,这个TraceId也会传递下去,直到调用链的末端。若是另一次调用链条,则会使用另一个随机生成的TraceId。

针对这种追踪机制,业界已经存在了一些较为成熟的方案,例如[Zipkin](https://zipkin.io/)能够很好的完成链路调用的追踪工作。

如果你使用的是Spring Boot全家桶,那么Zipkin可以较为方便地集成进来,可以参考[这篇教程](https://spring.io/blog/2016/02/15/distributed-tracing-with-spring-cloud-sleuth-and-spring-cloud-zipkin)。

本书将选择一种更为直接的方式:手写代码实现调用追踪,并将它整合进日志系统中。

这样做的好处有:
* 如果你用过Zipkin,就能发现,它并不能覆盖全部的代码。通过手写代码的方式,我们能够更细粒度的控制追踪的实现。
* ZipKin默认是需要独立存储的,对于常年运行的系统来说,无论是运维还是机器,都会造成一定的浪费。在我们的架构下,会把追踪与日志进行融合,节省Zipkin带来的额外成本。
* 打日志时会自动带上TraceId,让调试和定位问题更加方便。

## 利用Logback的MDC机制存储TraceId 

前面已经提到,我们想要将TraceId追加到日志系统中。

幸运的是,Logback中提供了[Mapped Diagnostic Context](https://logback.qos.ch/manual/mdc.html)的功能,我们可以将一些变量存储到MDC中,在打日志中,将它打印出来。

要说明的是,MDC是线程独立、线程安全的,而在我们的架构中,无论是HTTP还是RPC请求,都是在各自独立的线程中完成的,与MDC的机制可以很好地契合。

我们来看一下TraceId的存取:
```java

import org.slf4j.MDC;

public class TraceIdContext {

    public static final String TRACE_ID_KEY = "TRACE_ID";

    public static void setTraceId(String traceId) {
        if (traceId != null && !traceId.isEmpty()) {
            MDC.put(TRACE_ID_KEY, traceId);
        }
    }

    public static String getTraceId() {
        String traceId = MDC.get(TRACE_ID_KEY);
        if (traceId == null) {
            return "";
        }
        return traceId;
    }

    public static void removeTraceId() {
        MDC.remove(TRACE_ID_KEY);
    }
}
```

如上所示: 我们直接调用MDC的put, get , remove方法完成了traceId(TraceId)的存取

traceId可以根据需求随机生成:
```java

import java.util.Random;

/**
 * @author coder4
 */
public class TraceIdUtils {

    private static final Random random = new Random(System.currentTimeMillis());

    public static String getTraceId() {
        // 随机正整数的16进制化
        return Long.toString(Math.abs(random.nextLong()), 16);
    }

}
```

如上所属,我们随机生成正整数,并将其格式化为16进制字符串,方便查看。

至于TraceId的生成时机,我们稍后进行讨论。

## 调用TraceId的全新生成

根据前面的描述,应该可以想到,当TraceId为空的情况下,我们需要生成一个新的TraceId。

换句话说,当访问是"源头"的情况下,标志着一次追踪的开始,例如:
* HTTP请求开始之前
* 消息队列监听器接收新消息时


下面来看一下实现。首先,我们可以通过Filter机制,实现HTTP请求中的TraceId分配:
```java
import org.springframework.web.filter.AbstractRequestLoggingFilter;

import javax.servlet.http.HttpServletRequest;

public class TraceIdRequestLoggingFilter extends AbstractRequestLoggingFilter {
    @Override
    protected void beforeRequest(HttpServletRequest request, String message) {
        TraceIdContext.setTraceId(TraceIdUtils.getTraceId());
    }

    @Override
    protected void afterRequest(HttpServletRequest request, String message) {
        TraceIdContext.removeTraceId();
    }
}

```

如上所示,我们通过Spring MVC的AbstractRequestLoggingFilter接口,在发起请求之前生成一个全新的TraceId,并在请求结束后清理这个TraceId。

当然,上述Filter需要配合一个自动配置才能生效:
```java

nfiguration
@ConditionalOnWebApplication
public class TraceIdRequestLoggingFilterConfiguration {

    @Bean
    public TraceIdRequestLoggingFilter createTraceIdMDCFilter() {
        return new TraceIdRequestLoggingFilter();
    }

}
```

代码比较简单,不再详细讨论了。

在消息队列的事件监听器中,也可以采取类似的方法新建TraceId:
```java

public void onMessage(Message msg) {
    TraceIdContext.setTraceId(TraceIdUtils.getTraceId());
    // do message process
    TraceIdContext.removeTraceId();
}

```

当然,如果对每个事件监听器都做上述处理,未免有些麻烦,可以使用抽象基类或者AOP的方式统一,这里不再详细展开。

## TraceId的传递

前面说了TraceId的全新生成,在另外一些情况中,只需要继承环境中已有的TraceId,不需要重新生成,例如:
* RPC调用,一般情况是在HTTP请求中、或者消息队列中发起,此时系统中已有了一个TraceId
* 服务内各类之间的相互调用,由于并不是与外界隔离的入口,一般都已经存在了一个TraceId,所以也不需要生成。

前面已经提到,每次完整请求都是在各自独立的线程中完成的,因此"服务内各类之间"的相互调用,不需要额外处理,直接从MDC获取TraceId即可。

我们重点看一下RPC中,如何传递TraceId。

我们的技术架构使用了Thrife RPC,可以通过自定义协议的方式,将TraceId自动传递过去:
```java

import com.coder4.sbmvt.trace.TraceIdContext;
import com.coder4.sbmvt.trace.TraceIdUtils;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TField;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.protocol.TProtocolFactory;
import org.apache.thrift.protocol.TProtocolUtil;
import org.apache.thrift.protocol.TType;
import org.apache.thrift.transport.TTransport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author coder4
 */
public class TraceBinaryProtocol extends TBinaryProtocol {

    public static final short TRACE_ID_FIELD = Short.MAX_VALUE;

    private Logger LOG = LoggerFactory.getLogger(getClass());

    public TraceBinaryProtocol(TTransport trans) {
        super(trans);
    }

    public TraceBinaryProtocol(TTransport trans, boolean strictRead, boolean strictWrite) {
        super(trans, strictRead, strictWrite);
    }

    public TraceBinaryProtocol(TTransport trans, long stringLengthLimit,
                               long containerLengthLimit, boolean strictRead,
                               boolean strictWrite) {
        super(trans, stringLengthLimit, containerLengthLimit, strictRead, strictWrite);
    }

    @Override
    public void writeFieldStop() throws TException {
        // get traceId from context
        String traceId = TraceIdContext.getTraceId();
        if (traceId == null || traceId.isEmpty()) {
            // generate new one if not avaliable
            traceId = TraceIdUtils.getTraceId();
            TraceIdContext.setTraceId(traceId);
        }
        // parse traceId
        TField field = new TField("", TType.STRING, TRACE_ID_FIELD);
        writeFieldBegin(field);
        writeString(traceId);
        writeFieldEnd();
        // super
        super.writeFieldStop();
    }

    @Override
    public TField readFieldBegin() throws TException {
        // super
        TField field = super.readFieldBegin();
        // read traceId
        while (true) {
            switch (field.id) {
                case TRACE_ID_FIELD:
                    if (field.type == TType.STRING) {
                        // set traceId to context
                        String traceId = readString();
                        TraceIdContext.setTraceId(traceId);
                        readFieldEnd();
                    } else {
                        TProtocolUtil.skip(this, field.type);
                        LOG.error("traceId field type is not string");
                    }
                    break;
                default:
                    return field;
            }

            field = super.readFieldBegin();
        }
    }

    public static class Factory extends TBinaryProtocol.Factory implements TProtocolFactory {

        public Factory() {
            super();
        }

        public Factory(boolean strictRead, boolean strictWrite) {
            super(strictRead, strictWrite);
        }

        public Factory(boolean strictRead, boolean strictWrite, long stringLengthLimit, long containerLengthLimit) {
            super(strictRead, strictWrite, stringLengthLimit, containerLengthLimit);
        }

        @Override
        public TProtocol getProtocol(TTransport trans) {
            TraceBinaryProtocol protocol =
                    new TraceBinaryProtocol(trans, stringLengthLimit_, containerLengthLimit_,
                            strictRead_, strictWrite_);

            return protocol;
        }
    }
}


```

如上所示,我们继承了TBinaryProtocol,实现了TraceBinaryProtocol。

* 它在writeFieldStop即写完其他字段后,追加了一个特殊字段TRACE_ID。字段TRACE_ID对应的值,首先会从MDC中获取,若取不到则需要重新生成。
* 类似地在服务读取阶段,会检查有无TRACE_ID字段,若有将它写入到当前MDC环境中。

## TraceID的展示

经过上面的努力,在我们的架构下,所有请求相关的处理,都会自动带上一个TRACE_ID,我们再来看一下如何将其展示在日志中:


我们在logback的Pattern中添加"[tr=%mdc{TRACE_ID:-0}]"一项,表示从MDC中获取key为TRACE_ID的数据,若取不到则打印0。

完整的Pattern如下:
```xml
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] [%thread] [%logger] [tr=%mdc{TRACE_ID:-0}] %msg %n</Pattern>
```

经过上述修改后,你可以重启一下服务,访问REST或者RPC接口。

你会发现,不同的请求中,[tc=xxx]中的TraceId会发生变化。但在同一次请求中调用了多个类,则TraceId会保持、传递下去。


================================================
FILE: legacy/ms-monitor/README.md
================================================
# 微服务监控



================================================
FILE: legacy/ms-monitor/k8s-prometheus-grafana.md
================================================
# Kubernetes + Prometheus + Grafana监控平台

平台监控是微服务架构中的重要一环。

例如,一个很常见的场景,某个微服务突然响应变慢,之前都是100毫米内返回,现在需要2秒才能返回结果,导致大量下游服务超时,究竟出了什么问题呢?

可能的原因有很多,举几个常见的例子:
* 物理机出现了问题,被别的任务影响,占用了CPU。
* 微服务的某一个副本出现了Bug,导致死循环,响应变慢
* 微服务出现Full GC,导致响应变慢

可能的原因还有很多,那么,究竟是哪种原因导致的呢?

我们可以通过日志去查找,但查找起来很费时。而且,对于收到物理机影响等部分请款,是无法在日志中体现出来的。

此时,就是监控平台大显身手的时刻了。监控系统会收集系统中的各项性能指标,按照类型及时间进行聚合,并通过图形化界面的方式呈现出来,让我们对系统的基本运行状况一目了然,便于快速发现当前问题、查找历史问题。

对于监控平台,已经有很多优秀的开源解决方案,例如传统的Zabbix、Nagios,但这些系统比较复杂,一般需要较为专业的运维人员才能上手。

本书选用较为轻量级的Prometheus + Grafana实现监控平台。

Prometheus是一款开源的性能监控、预警系统,他的特点有:
* 多维数据源、时间聚合
* 支持高级查询语句
* 支持单击和多级存储,不依赖其他分布式系统
* 数据源、服务器均支持多种配置、自动发现方式

上述特点没有提到可视化部分,是的,你已经猜到了,Prometheus只负责收集、存储、查询数据,并不包含数据可视化的部分。

一般可以通过Grafana实现监控数据的可视化,效果还是非常炫酷的,如下图所示:

![Grafana可视化](./grafana-node.png)

上图通过曲线图和仪表盘的方式,展示了k8s集群中,某台物理机的性能状况,CPU、系统负载、内存、网络等状况一目了然。

本节将主要探讨如何将Kuberntes、Prometheus、Grafana整合在一起。

## 前期准备

在整合监控平台前,你首先需要有一个真正的Kubernetes集群,我们假设你已经搭建了一个有两个结点的集群,一台是master,另一台是slave。

如果你不了解如何搭建k8s集群,可以参考[《搭建Kubernets集群》](../devops/k8s-cluster.md)一节。

在本文提供的方案中,Grafana和Prometheus默认都是架设在Kubernetes集群内的,因此,你需要打通本地和集群网络,如果你没有头绪,可以参考[《OpenVPN访问Kubernetes集群内网》](../devops/openvpn-k8s.md)一节。

上述技术准备妥当后,我们来开始搭建监控平台。

## 搭建监控平台

首先,我们要安装helm,这是Kubernetes的包管理系统,类似于Ubuntu中apt的地位。

```shell
wget https://storage.googleapis.com/kubernetes-helm/helm-v2.9.1-linux-amd64.tar.gz
tar -xvf ./helm-v2.9.1-linux-amd64.tar.gz
cd linux-amd64/

```

helm需要初始化才能工作,但在初始化前,先需要在rbac中进行授权, tiller-rbac.yaml:
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: tiller
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: tiller
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
  - kind: ServiceAccount
    name: tiller
    namespace: kube-system
```

授权比较简单
```shell

kubectl apply -f ./tiller-rbac.yaml

```

有了权限后,我们进行初始化,并添加一下第三方源
```shell
helm init --service-account tiller
helm repo add coreos https://s3-eu-west-1.amazonaws.com/coreos-charts/stable/
```

上述执行成功后,我们开始创建prometheus:
```shell
helm install coreos/prometheus-operator --name prometheus-operator --namespace monitoring
```

创建成功后,再创建grafana,并配置其和prometheus关联:
```shell
helm install coreos/kube-prometheus --name kube-prometheus --set global.rbacEnable=true --namespace monitoring
```

由于涉及到的Pod比较多,下载镜像的时间比较长,全部下载完成后,状态应该如下所示:
```shell

kubectl get pod -n monitoring

NAME                                                  READY     STATUS    RESTARTS   AGE
alertmanager-kube-prometheus-0                        2/2       Running   0          10m
kube-prometheus-exporter-kube-state-66bccfc84-x4ngb   2/2       Running   0          10m
kube-prometheus-exporter-node-62phq                   1/1       Running   0          10m
kube-prometheus-exporter-node-lt954                   1/1       Running   0          10m
kube-prometheus-grafana-f869c754-44f9k                2/2       Running   0          10m
prometheus-kube-prometheus-0                          3/3       Running   1          10m
prometheus-operator-858c485-fkcjz                     1/1       Running   0          1h

```

对外暴露的Service如下:
```shell
kubectl get service -n monitoring
NAME                                  TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)             AGE
alertmanager-operated                 ClusterIP   None             <none>        9093/TCP,6783/TCP   11m
kube-prometheus                       ClusterIP   10.111.94.74     <none>        9090/TCP            11m
kube-prometheus-alertmanager          ClusterIP   10.109.44.85     <none>        9093/TCP            11m
kube-prometheus-exporter-kube-state   ClusterIP   10.105.121.198   <none>        80/TCP              11m
kube-prometheus-exporter-node         ClusterIP   10.96.155.209    <none>        9100/TCP            11m
kube-prometheus-grafana               ClusterIP   10.109.181.200   <none>        80/TCP              11m
prometheus-operated                   ClusterIP   None             <none>        9090/TCP            11m

```

其中,kube-prometheus-grafana的10.109.181.200就是Grafana的Service IP地址。

## 监控系统展示

在打通k8s集群内网后,我们直接打开浏览器访问"10.109.181.200",即可进入Grafana图形化监控系统。

![Grafana可视化](./grafana-pod.png)

![Grafana可视化](./grafana-statefulset.png)

如上所示,默认会从物理机(node)、容器(pod)、容器合集(statefulset)、k8s集群四个层次展示,点击左上角的按钮可以切换展示级别。点击顶部筛选条可以切换具体的机器、容器等实体。

## 拓展与思考
1. 在本文中,我们实现了对集群中资源、实体的监控,在实际应用中,还想对微服务进行监控,例如REST接口的.99响应、错误码4xx、5xx数量。如何完成这项工作呢?请自行查找资料,并实现这类功能。
1. Prometheus除了收集监控指标外,还支持报警。如果我们想实现物理机内存占用大于90%,自动发邮件(或短信)报警,如何实现呢?请自行查找资料,实现这类功能。


================================================
FILE: legacy/ms-monitor/sb-prometheus.md
================================================
# 整合Prometheus



================================================
FILE: legacy/ms-monitor/sb-sentry.md
================================================
# Spring Boot整合Sentry

在上一小节中,我们探讨了如何运维Senty系统。

搭建好的Sentry系统,需要接入错误事件的数据源,才能发挥功效。

在本节中,我们探讨如何将Spring Boot的微服务项目与Sentry整合起来。

## Sentry中新建项目

Sentry中可以新建若干个项目,对应于若干微服务。

而同一微服务的若干副本应该向同一个Sentry项目发送错误信息的数据,这些副本可以通过来源IP区分。

我们来新建一个Sentry项目,如下图所示:

![Sentry中新建项目](./sentry-create-proj.png)

首先选择一个项目类型,这里选择Java

接着,选择项目名称,我们这里和微服务的名字保持一致lmsia-abc。

新建好项目后,首页会出现项目的列表,及24小时内,该项目的错误事件数量,如下图所示:

![Sentry首页展示项目](./sentry-proj2.png)

## 在Spring Boot中配置日志输出

Sentry提供了丰富多样的接入方式。

在本书中我们采用了Spring Boot默认的logback作为日志系统,Sentry也支持这种方式。

首先添加依赖:
```grovvy
    compile 'io.sentry:sentry-logback:1.7.5'
```

然后将在logback的配置修改如下:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <!-- For console -->
    <appender name="ConsoleAppender" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <charset>UTF-8</charset>
            <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%level] [%thread] [%logger] [tr=%mdc{TRACE_ID:-0}] %msg %n</Pattern>
        </encoder>
    </appender>

    <!-- For file with daily rollover -->
    <appender name="ServerFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <charset>UTF-8</charset>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%level] [%thread] [%logger] [tr=%X{TRACE_ID:-0}] %msg %n</pattern>
        </encoder>

        <file>/app/logs/lmsia-abc.log</file>

        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- daily rollover with gz -->
            <fileNamePattern>/app/logs/lmsia-abc.%d{yyyy_MM_dd}.log.gz</fileNamePattern>
            <!-- keep 30 days' max -->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
    </appender>

    <!-- For sentry -->
    <appender name="SentryAppender" class="io.sentry.logback.SentryAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
    </appender>

    <!-- console only if local active -->
    <springProfile name="local">
        <root level="INFO">
            <appender-ref ref="ConsoleAppender"/>
        </root>
    </springProfile>
    <!-- file,sentry only if test or online active -->
    <springProfile name="test,online">
        <root level="INFO">
            <appender-ref ref="ServerFileAppender"/>
            <appender-ref ref="SentryAppender"/>
        </root>
    </springProfile>

</configuration>
```

与之前的配置文件相比,主要改动为:
* 新增SentryAppender。它会自动沿用之前最近的一个Pattern,及这里的ServerFileAppender。另外,只有ERROR级别的LOG,才会加到这个Appender中。
* 当test或者online的profile激活时,自动上报。你可能希望test和online上报到不同的项目中,别着急,我们后面解决这种情况。

经过上述配置后,每当发生ERROR级别的LOG,就会追加到SentryAppender中。

## Spring Boot中配置DSN

在刚才的配置中,我们并没有制定Sentry服务的IP和端口,如何让Spring Boot知道事件要发送到哪里呢?

此外前面提到,Sentry中支持配置多个项目,如何告诉Spring Boot要发送到Sentry的哪个项目中呢?

这就需要DSN出场了,DSN是Sentry创建项目时生成的一个“KEY”,用于标识和区分不同的项目。

可以在项目的Setting中找到它,如下图所示:

![Sentry项目的DSN](./sentry-dsn.png)

然后,我们在Spring Boot项目的resources目录下,新建sentry.properties文件
```
dsn = http://9445296bd1a5441c8988af84044890a3@sentry-host:sentry-port/2
```

如上配置后,就可以识别出sentry服务的位置已经对应的项目名了。

要说明的是,sentry-host和sentry-port要根据你的需要自行修改,可以是IP也可以是可DNS解析的域名。

如果你想要区分test和online环境,可以如下操作:
* 建立不同的Sentry服务,或者同一个Senty服务下建立不同的项目
* 根据不同的profile分别创建sentry.properties文件,如sentry.properties.test, sentry.properties.online,里面配置不同的dsn key
* 打Docker镜像时加载不同的文件,并重命名为sentry.properties

## 实验异常发生的效果

为了实验效果,我们在微服务代码中,主动抛出一个异常,然后看一下Sentry的lmsia-abc项目:

![Sentry项目的异常预警](./sentry-err.png)

可以看到,我们的异常被Sentry捕获,并显示了出来。如果同样的ERROR级别LOG发生了多次,还会自动聚合。

至此,我们完成了Spring Boot与Sentry的整合工作。


================================================
FILE: legacy/ms-monitor/sentry-devops.md
================================================
# Sentry 错误预警系统的运维

在上一章中,我们介绍了EBLK的日志分析平台。

在日志分析平台上,我们可以很方便的查找系统的日志。然而,EBLK并总是能满足需求:
* 日志绝大多数是INFO等级的,即信息日志。如果系统运行出现问题,我们想从中查找,实际希望的是找到ERROR类型的或者异常信息。
* 我们经常需要排查一些历史故障,即需要增加时间维度的信息
* 我们希望经常出现的类似错误,能够聚合在一起,方便我们排查

Sentry是一个实时的事件日志和聚合平台,我们可以通过配置,把所有ERROR类型的日志发送给Sentry保存下来,并通过其聚合结果,迅速的定位线上问题。

在本节中,我们将探讨Sentry预警系统的运维工作。

由于Sentry服务本身比较复杂,涉及多个步骤初始化步骤及多个Volume,因此我们不再采用Kubernetes集群,而是通过Docker直接部署在某台物理机上。

## Sentry存储依赖的部署

Sentry服务需要使用两个存储:Redis、Postgres,我们首先启动这两个依赖服务。

首先创建redis, sentry-redis.sh:
```shell
#!/bin/bash

NAME="sentry-redis"
VOLUME="/home/coder4/docker_data/sentry-redis"

# make sure volume valid 
mkdir -p $VOLUME && sudo chmod -R 777 $VOLUME

# kill old and run new 
docker ps -q -a --filter "name=$NAME" | xargs -I {} docker rm -f {}
docker run \
    --hostname $NAME \
    --name $NAME \
    --volume "$VOLUME":/data \
    --detach \
    --restart always \
    redis:4 
```

如上所述:
* 创建了基于redis 4的容器
* 设置volume到/data,这样,重启redis并不会导致数据丢失

接着,我们创建Postegres数据库, sentry_postgres.sh:
```shell
#!/bin/bash

NAME="sentry-postgres"
VOLUME="/home/coder4/docker_data/sentry-postgres"

POSTGRES_DB_USER="sentry"
POSTGRES_DB_PASS="sentry_pass"

# make sure volume valid 
sudo mkdir -p $VOLUME && sudo chmod -R 777 $VOLUME

# kill old and run new 
docker ps -q -a --filter "name=$NAME" | xargs -I {} docker rm -f {}
docker run \
    --hostname $NAME \
    --name $NAME \
    --volume "$VOLUME":/var/lib/postgresql/data \
    --env POSTGRES_USER=$POSTGRES_DB_USER \
    --env POSTGRES_PASSWORD=$POSTGRES_DB_PASS \
    --detach \
    --restart always \
    postgres:10 

```

## Sentry 服务的部署 

在正式部署Sentry之前,首先要生成key:

```shell
docker run --rm sentry config generate-secret-key
```

输出结果是
```
q5%_k4t#w#43rlnd(mr1ms%p8(mjofh9z&4al8d1q&a3f#19_d
```

记住这个key,我们马上会用到。

下面,我们对Sentry进行初始化:
```shell
#!/bin/bash

NAME="sentry-main"
REDIS_LINK="sentry-redis:redis"
POSTGRES_LINK="sentry-postgres:postgres"

SENTRY_SECRET="q5%_k4t#w#43rlnd(mr1ms%p8(mjofh9z&4al8d1q&a3f#19_d"

# kill old and run new 
docker ps -q -a --filter "name=$NAME" | xargs -I {} docker rm -f {}
docker run \
    --hostname $NAME \
    --name $NAME \
    --env SENTRY_SECRET_KEY=$SENTRY_SECRET \
    -it \
    --restart always \
    --link $REDIS_LINK \
    --link $POSTGRES_LINK \
    sentry:9 upgrade

```

在执行db操作一段时间后,会有一个交互,如下:
```
Email: lihy@coder4.com
Password: 
Repeat for confirmation: 
Should this user be a superuser? [y/N]: y
User created: lihy@coder4.com
...
```
创建好的用户,就是之后默认的管理员用户

初始化完毕后,我们正式创建Sentry:
```shell
#!/bin/bash

NAME="sentry-main"
REDIS_LINK="sentry-redis:redis"
POSTGRES_LINK="sentry-postgres:postgres"

SENTRY_SECRET="q5%_k4t#w#43rlnd(mr1ms%p8(mjofh9z&4al8d1q&a3f#19_d"

# kill old and run new 
docker ps -q -a --filter "name=$NAME" | xargs -I {} docker rm -f {}
docker run \
    --hostname $NAME \
    --name $NAME \
    -p 8080:9000 \
    --env SENTRY_SECRET_KEY=$SENTRY_SECRET \
    --detach \
    --restart always \
    --link $REDIS_LINK \
    --link $POSTGRES_LINK \
    sentry:9

```

与上面的初始化相比,脚本基本是类似的,差别在于:
* 使用的是后台运行detach而非-it交互模式
* 没有执行upgrade命令
* 开放了8080端口

执行成功后,我们访问http://localhost:8080,即可成功进入登录界面,如下图所示:

![Sentry登录界面](./sentry-login.png)

如果你现在登录的话,会提示"Background workers havn't checked in recently...",即后台收集进程&定时任务没有启动。

我们首先来启动收集进程:

```shell
#!/bin/bash

NAME="sentry-worker"
REDIS_LINK="sentry-redis:redis"
POSTGRES_LINK="sentry-postgres:postgres"

SENTRY_SECRET="q5%_k4t#w#43rlnd(mr1ms%p8(mjofh9z&4al8d1q&a3f#19_d"

# kill old and run new 
docker ps -q -a --filter "name=$NAME" | xargs -I {} docker rm -f {}
docker run \
    --hostname $NAME \
    --name $NAME \
    --env SENTRY_SECRET_KEY=$SENTRY_SECRET \
    --detach \
    --restart always \
    --link $REDIS_LINK \
    --link $POSTGRES_LINK \
    sentry:9 run worker

```

然后启动定时任务:
```shell
#!/bin/bash

NAME="sentry-cron"
REDIS_LINK="sentry-redis:redis"
POSTGRES_LINK="sentry-postgres:postgres"

SENTRY_SECRET="q5%_k4t#w#43rlnd(mr1ms%p8(mjofh9z&4al8d1q&a3f#19_d"

# kill old and run new 
docker ps -q -a --filter "name=$NAME" | xargs -I {} docker rm -f {}
docker run \
    --hostname $NAME \
    --name $NAME \
    --env SENTRY_SECRET_KEY=$SENTRY_SECRET \
    --detach \
    --restart always \
    --link $REDIS_LINK \
    --link $POSTGRES_LINK \
    sentry:9 run cron 

```

启动后稍等一会,我们用初始化时设置的用户名、密码登录系统,进入初始化界面:

![Sentry初始配置界面](./sentry-config.png)

配置如下:
* Root URL写入一个可以DNS解析的域名,例如sentry.coder4.com
* Admin Email写入任意一个邮件地址,例如sentry@coder4.com

配置好后,进入如下的登台添加新项目界面,至此,Sentry的部署工作配置完成。

![Sentry完成界面](./sentry-ready.png)

## 思考与拓展
* 在本节的部署中,我们使用了默认的账户配置模式。前面介绍过,在团队内部,应该使用统一的帐号系统以提升效率。如何让Sentry系统接入LDAP帐号服务呢,请自行查找资料,实现这个功能。


================================================
FILE: legacy/ms-monitor/sentry.txt
================================================
https://laravel-china.org/articles/4285/build-your-own-sentry-service


================================================
FILE: legacy/ms-msgq/README.md
================================================
# 微服务的消息队列



================================================
FILE: legacy/ms-msgq/dev-kafka.md
================================================
# Kafka 流处理开发简介

在上一节,我们介绍了分布式流处理平台Kafka的运维工作,在这一节,我们将讨论Kafka的应用开发。

你可能已经注意到,这一节的标题并不是"在微服务中的集成",而是"开发简介"。

使得,如在前文所述,Kafka的吞吐性能出色,但延迟性能一般,因此多用于离线处理的场景,典型应用有:
* 异源数据[^1]的同步、转换、备份。即"数据搬运工"
* 处理从多处收集的日志
* Event Sourcing模式的事件回放

对于数据搬运工类的需求,建议优先考虑[Kafka Connect](http://kafka.apache.org/documentation/#connect_overview)。这是Kafka官方提供的一组工具集,可以通过简单的配置,就完成数据的同步和一些转换的工作。

对于Event Sourcing的模式开发,和日志处理收集的开发模式基本一致。

在此,我们用较短的篇幅,对Kafka的开发做一个简要的介绍。

在研究代码之前,有一些必须明确的基本概念:
* Topics: 可以理解为队列,同种消息应放入相同的Topic内。
* Partition: Topic下可划分为多个分区, 便于并行处理。
* Replicas: Topic的Partition可以划分为多个副本,从而实现高可用保证。
* Producer: 消息的生产者。
* Consumer: 消息的消费者。
* Consumer Group: 每个消费者可以设定一个Consumer Group。Kafka保证: 同一个消息,只投递给相同Consumer Group中的一台Consumer。换句话说,注册在同一个Consumer Group下的Consumer,可以并行的处理消息。

下面,我们看一下生产者
```
//import util.properties packages
import java.util.Properties;

//import simple producer packages
import org.apache.kafka.clients.producer.Producer;

//import KafkaProducer packages
import org.apache.kafka.clients.producer.KafkaProducer;

//import ProducerRecord packages
import org.apache.kafka.clients.producer.ProducerRecord;

//Create java class named “SimpleProducer"
public class SimpleProducer {
   
   public static void main(String[] args) throws Exception{
      
      // Check arguments length value
      if(args.length == 0){
         System.out.println("Enter topic name");
         return;
      }
      
      //Assign topicName to string variable
      String topicName = args[0].toString();
      
      // create instance for properties to access producer configs   
      Properties props = new Properties();
      
      //Assign localhost id
      props.put("bootstrap.servers", "localhost:9092");
      
      //Set acknowledgements for producer requests.      
      props.put("acks", "all");
      
      //If the request fails, the producer can automatically retry,
      props.put("retries", 0);
      
      //Specify buffer size in config
      props.put("batch.size", 16384);
      
      //Reduce the no of requests less than 0   
      props.put("linger.ms", 1);
      
      //The buffer.memory controls the total amount of memory available to the producer for buffering.   
      props.put("buffer.memory", 33554432);
      
      props.put("key.serializer", 
         "org.apache.kafka.common.serializa-tion.StringSerializer");
         
      props.put("value.serializer", 
         "org.apache.kafka.common.serializa-tion.StringSerializer");
      
      Producer<String, String> producer = new KafkaProducer
         <String, String>(props);
            
      for(int i = 0; i < 10; i++)
         producer.send(new ProducerRecord<String, String>(topicName, 
            Integer.toString(i), Integer.toString(i)));
               System.out.println("Message sent successfully");
               producer.close();
   }
}

```

简单解释下代码:
1. 连接localhost:9092
1. 收到消息后自动ack
1. 设置缓存大小等配置
1. 消息的key和value都是string类型,配置对应的序列化类
1. 发送10个消息

下面来看一下Consumer代码
```java
import java.util.Properties;
import java.util.Arrays;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.ConsumerRecord;

public class ConsumerGroup {
   public static void main(String[] args) throws Exception {
      if(args.length < 2){
         System.out.println("Usage: consumer <topic> <groupname>");
         return;
      }
      
      String topic = args[0].toString();
      String group = args[1].toString();
      Properties props = new Properties();
      props.put("bootstrap.servers", "localhost:9092");
      props.put("group.id", group);
      props.put("enable.auto.commit", "true");
      props.put("auto.commit.interval.ms", "1000");
      props.put("session.timeout.ms", "30000");
      props.put("key.deserializer",          
         "org.apache.kafka.common.serializa-tion.StringDeserializer");
      props.put("value.deserializer", 
         "org.apache.kafka.common.serializa-tion.StringDeserializer");
      KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
      
      consumer.subscribe(Arrays.asList(topic));
      System.out.println("Subscribed to topic " + topic);
      int i = 0;
         
      while (true) {
         ConsumerRecords<String, String> records = con-sumer.poll(100);
            for (ConsumerRecord<String, String> record : records)
               System.out.printf("offset = %d, key = %s, value = %s\n", 
               record.offset(), record.key(), record.value());
      }     
   }  
}

```

基本的配置与Producer类似,这里i不再重复了,能够并行处理的关键是"group.id"这个参数。
如果同时启动2个Consumer进程,会发现消息是在两个Consumer进程中,交替输出的。

上述代码参考自[Tutorial Spoint的Kafka教程](https://www.tutorialspoint.com/apache_kafka/),这是一部非常好的Kafka教程,如果你没有时间完整阅读官方文档,强烈推荐你读一下这部教程。

[^1]: 异源指的是不同数据源,例如MySQL和Kafka之间、HDFS和Kafka之间。


================================================
FILE: legacy/ms-msgq/kafka-devops.md
================================================
# Kafka流处理平台的运维

Kafka是高性能的分布式流处理平台,它的特点有:
* 类似于传统的消息队列,为海量流式数据提供了消息发布/订阅模型。
* 支持容错的流式数据存储。
* 流式数据的实时处理。

Kafka是一款吞吐性能非常优秀的分布式流处理系。虽然吞吐性能优秀,但Kafka的处理延迟较高,一般多用于日志等离线处理,不会用于实时的消息队列系统。

本节将讨论Kafka集群的部署。

与我们之前讨论的MySQL、Memcached等组件稍有不同:
* Kafka对I/O资源消耗较大,使用Volume挂载的方式,存在一定性能损耗。
* 并且Kafka本身内置了高可用、集群的功能。
* Kafka依赖Zookeeper,后者对资源波动较为敏感,一般需要独立部署。

综上所述,对于Kafka和其依赖的ZooKeeper,我们将在服务器上独立部署,而不会将其部署在Kubernetes集群中。

## 准备Java环境

我们假设你手里的是仅安装了操作系统的"裸机"服务器,在这里,我们以以Ubuntu 18.04为例进行讲解。

首先准备Java的apt源

```shell
sudo add-apt-repository -y ppa:webupd8team/java
sudo apt update
```

然后,自动同意许可、自动安装
```shelll
echo debconf shared/accepted-oracle-license-v1-1 select true | sudo debconf-set-selections
echo debconf shared/accepted-oracle-license-v1-1 seen true | sudo debconf-set-selections
sudo apt install -y oracle-java8-set-default
```

安装好后,我们验证一下
```shell
java -version
java version "1.8.0_171"
Java(TM) SE Runtime Environment (build 1.8.0_171-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.171-b11, mixed mode)
```

部署Kafka集群至少需要6台机器,3台给Zookeeper,另外3台给Kafka的Broker。

为了说明方便,我们假设6台机器的主机名分别为zk1~zk3,kafka1~kafka3

请在6台机器上都进行上述Java 8的安装。

## 准备主机环境

zk和kafka集群的部署,都依赖2个先决条件:
* 主机之间必须支持内网访问
* 内网可以通过hostname直接访问

由于是在同一个机房内部署,所以我们假设上述条件1是满足的。

对于条件2,有多种实现方案,我们这里采用最传统的hostname修改方式。

对于上述6台主机,内网IP分别为:
* z1~zk3:192.168.0.11 ~ 192.168.0.13
* kafka1~kafka3:192.168.0.21 ~ 192.168.0.23

则我们修改6台主机的hosts文件如下:
```shell
sudo vim /etc/hosts

127.0.0.1    localhost
#127.0.1.1    zk2

# The following lines are desirable for IPv6 capable hosts
::1          localhost ip6-localhost ip6-loopback
ff02::1      ip6-allnodes
ff02::2      ip6-allrouters

192.168.0.11 zk1
192.168.0.12 zk2
192.168.0.13 zk3

192.168.0.21 kafka1
192.168.0.22 kafka2
192.168.0.23 kafka3
```

修改后,任意一台机器应该都可以通过hostname来ping通其他主机,例如:
```shell
zk1$ ping kafka2

PING baidu.com (192.168.0.22) 56(84) bytes of data.
64 bytes from 192.168.0.22: icmp_seq=1 ttl=47 time=10.0 ms
...

```

注意,在上面的配置中,我们还去掉127.0.1.1的映射,最终效果是ping也会返回内网ip,而不是127.0.1.1:
```shell
ping zk2
PING zk2 (192.168.0.12) 56(84) bytes of data.
64 bytes from zk2 (192.168.0.12): icmp_seq=1 ttl=64 time=0.046 ms

```


## 部署Zookeeper

接下来,我们将在zk1~zk3上部署zookeeper,请确认这三台机器已经安装了Java 8。

首先为zookeeper创建本地用户,在zk1~zk3上分别执行:
```shell
useradd -m zookeeper

```

下载并解压到本地,同样在zk1~zk3上分别执行:
```shell
su zookeeper
cd $HOME

wget https://archive.apache.org/dist/zookeeper/zookeeper-3.4.12/zookeeper-3.4.12.tar.gz
tar -xzvf ./zookeeper-3.4.12.tar.gz
ln -s zookeeper-3.4.12 zookeeper

mkdir /home/zookeeper/zookeeper_data
```

注意最后创建了一个文件夹,用于储存zk的数据文件

为zk1和zk3添加不同的id
```shell
zookeeper@zk1:~$ echo "1" > /home/zookeeper/zookeeper_data/myid
zookeeper@zk2:~$ echo "2" > /home/zookeeper/zookeeper_data/myid
zookeeper@zk3:~$ echo "3" > /home/zookeeper/zookeeper_data/myid
```

在zk1~zk3上分别添加配置
```shell
vim /home/zookeeper/zookeeper/conf/zoo.cfg

tickTime=2000
dataDir=/home/zookeeper/zookeeper_data
clientPort=2181
initLimit=5
syncLimit=2
server.1=zk1:2888:3888
server.2=zk2:2888:3888
server.3=zk3:2888:3888
```

在zk1~zk3上启动zookeeper
```shell
cd /home/zookeeper/zookeeper/bin

./zkServer.sh start
```

启动后,可以在zookeeper.out中查看错误输出日志。

如果一切正常,我们用客户端尝试连接。
```shell
./zookeeper/bin/zkCli.sh -server zk1:2181,zk2:2181,zk3:2181

....
[zk: zk1:2181,zk2:2181,zk3:2181(CONNECTED) 0]
...
```

如上如果能显示"CONNECTED",就是连接成功了。

我们尝试创建结点,也能成功:
```shell

[zk: zk1:2181,zk2:2181,zk3:2181(CONNECTED) 5] create /hello world
Created /hello

[zk: zk1:2181,zk2:2181,zk3:2181(CONNECTED) 6] get /hello         
world
cZxid = 0x100000002
ctime = Tue Jun 12 11:36:22 UTC 2018
mZxid = 0x100000002
mtime = Tue Jun 12 11:36:22 UTC 2018
pZxid = 0x100000002
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
```

至此,我们已经完成了zookeeper集群的配置。

## Kafka集群配置

首先为kafka创建本地用户,在kafka1~kafka3上分别执行:
```shell
useradd -m kafka 

```

下载kafka并解压缩
```shell
su kafka
cd $HOME

wget http://www-eu.apache.org/dist/kafka/1.1.0/kafka_2.11-1.1.0.tgz
tar -xzvf kafka_2.11-1.1.0.tgz

ln -s kafka_2.11-1.1.0 kafka 
```

创建数据目录
```shell
mkdir /home/kafka/kafka_logs
```

配置文件(kafka1)
```shell
vim kafka/config/server.properties

broker.id=1
zookeeper.connect=zk1:2181,zk2:2181,zk3:2181
listeners=PLAINTEXT://kafka1:9092
log.dirs=/home/kafka/kafka_logs
```

分别在kafka1~kafka3上启动
```shell
cd $HOME

kafka/bin/kafka-server-start.sh -daemon ./kafka/config/server.properties 
```

创建队列(topic)
```shell
kafka/bin/kafka-topics.sh --create --zookeeper zk1:2181,zk2:2181,zk3:2181 --replication-factor 2 --partitions 3 --topic topic1
Created topic "topic1".
```

查看队列(topic)
```shell
kafka/bin/kafka-topics.sh --describe --zookeeper zk1:2181,zk2:2181,zk3:2181 --topic topic1

Topic:topic1	PartitionCount:3	ReplicationFactor:2	Configs:
	Topic: topic1	Partition: 0	Leader: 2	Replicas: 2,1	Isr: 2,1
	Topic: topic1	Partition: 1	Leader: 1	Replicas: 1,2	Isr: 1,2
	Topic: topic1	Partition: 2	Leader: 2	Replicas: 2,1	Isr: 2,1

```

列出所有队列(topic)
```shell
kafka/bin/kafka-topics.sh --list --zookeeper zk1:2181,zk2:2181,zk3:2181

topic1
```

生产消息
```shell
kafka/bin/kafka-console-producer.sh --broker-list kafka1:9092,kafka2:9092,kafka3:9092 --topic topic1
>a
>b

```

消费消息
```shell
kafka/bin/kafka-console-consumer.sh --zookeeper zk1:2181,zk2:2181,zk3:2181 --topic topic1 --from-beginning

a
b
```

删除队列
```shell
kafka/bin/kafka-topics.sh --delete --zookeeper zk1:2181,zk2:2181,zk3:2181 --topic topic1

Topic topic1 is marked for deletion.

```

至此,我们完成了Kafka的集群配置,更多内容可以参考[Kafka 官方文档](https://kafka.apache.org/documentation/)。


================================================
FILE: legacy/ms-msgq/rabbitmq-devops.md
================================================
# RabbitMQ 消息队列

RabbitMQ支持单机部署,也提供了"高可用"的集群部署方式,以提升性能和(或)可用性。

RabbitMQ支持两种形式的集群模式:
* 普通模式: 默认的模式。消息队列的数据结构存在于每个节点上,但实际消息只存储于某一个节点上。这种模式的优点是性能较高。缺点是,一旦存储数据的节点挂掉,消息就暂时不可用了,需要节点启动后才能恢复。
* 镜像模式: 在普通模式的基础上可配置需要镜像的队列。配置队列后,消息数据会自动同步到集群中的每个节点上。该模式的优点是可用性高,缺点是性能相对较低。

在本节,我们将首先配置普通模式的集群,然后配置镜像队列。

## Kubernetes下配置RabbitMQ集群

与Memcached类似,RabbitMQ集群中的节点,是相互独立的,而不是可替代的,因此我们也使用StatefulSet。

然而,RabbitMQ比Memcached更为复杂,他需要磁盘存储,即在StatefulSet上的Volume。这种情况下,是无法直接创建Volume挂载点的,而是需要先手动创建Persistent Volume(简称PV),然后再通过Persistent Volume Claim(简称PVC)去关联创建可用的PV。上述过程可以参考Kubernetes的[Persistent Volume文档](https://kubernetes.io/docs/concepts/storage/persistent-volumes/)。

我们首先创建3个PV, pvs.yaml:
```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv001 
spec:
  storageClassName: standard
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 20Gi
  hostPath:
    path: /data/pv001/
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv002 
spec:
  storageClassName: standard
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 20Gi
  hostPath:
    path: /data/pv002/
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv003
spec:
  storageClassName: standard
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 20Gi
  hostPath:
    path: /data/pv003/
```

在这里,我们使用的是minikube做演示,因而直接创建基于路径的Volume,在实际生产中,你可能需要创建支持自动迁移的Volume,具体可以参考[Storage Classes](https://kubernetes.io/docs/concepts/storage/storage-classes/)。

我们应用一下:
```yaml
kubectl apply -f pvs.yaml

persistentvolume "pv001" created 
persistentvolume "pv002" created 
persistentvolume "pv003" created 
```

在创建RabbitMQ集群之前,我们先要针对rabnbitmq这个metadata修改rbac。rbac是Kubernetes的安全性访问限制,具体原因可以参考这个[Issue](https://github.com/rabbitmq/rabbitmq-peer-discovery-k8s/issues/15)

rabbitmq-rbac.yaml:
```yaml
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: rabbitmq
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: rabbitmq
rules:
- apiGroups: [""]
  resources: ["endpoints"]
  verbs: ["get"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: rabbitmq
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: rabbitmq
subjects:
- kind: ServiceAccount
  name: rabbitmq

```

上述修改了rabbitmq的ServiceSccount、Role、RoleBinding默认安全设置,简要来说是允许其访问get和endpoints这两个Kubernetes提供的API。

我们应用上述修改,修改成功:
```yaml
kubectl apply -f ./rabbitmq-rbac.yaml

serviceaccount "rabbitmq" configured
role.rbac.authorization.k8s.io "rabbitmq" configured
rolebinding.rbac.authorization.k8s.io "rabbitmq" configured

```

在创建了pv,修改了rbac后,可以创建rabbitmq集群了,我们来看一下描述文件,rabbitmq-service.yaml:
```yaml
kind: Service
apiVersion: v1
metadata:
  name: rabbitmq
  labels:
    app: rabbitmq
spec:
  type: NodePort
  ports:
  - name: http
    protocol: TCP
    port: 15672
    targetPort: 15672
    nodePort: 31672
  selector:
    app: rabbitmq
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: rabbitmq
spec:
  selector:
    matchLabels:
      app: rabbitmq
  serviceName: rabbitmq
  replicas: 3
  template:
    metadata:
      labels:
        app: rabbitmq
    spec:
      serviceAccountName: rabbitmq
      terminationGracePeriodSeconds: 10
      containers:        
      - name: rabbitmq-autocluster
        image: pivotalrabbitmq/rabbitmq-autocluster
        ports:
          - name: http
            protocol: TCP
            containerPort: 15672
          - name: amqp
            protocol: TCP
            containerPort: 5672
        livenessProbe:
          exec:
            command: ["rabbitmqctl", "status"]
          initialDelaySeconds: 30
          timeoutSeconds: 5
        readinessProbe:
          exec:
            command: ["rabbitmqctl", "status"]
          initialDelaySeconds: 10
          timeoutSeconds: 5
        volumeMounts:
        - mountPath: /var/lib/rabbitmq/mnesia
          name: rabbitmq-pvc
        env:
          - name: MY_POD_IP
            valueFrom:
              fieldRef:
                fieldPath: status.podIP
          - name: RABBITMQ_USE_LONGNAME
            value: "true"
          - name: RABBITMQ_NODENAME
            value: "rabbit@$(MY_POD_IP)"
          - name: AUTOCLUSTER_TYPE
            value: "k8s"
          - name: AUTOCLUSTER_DELAY
            value: "10"
          - name: K8S_ADDRESS_TYPE
            value: "ip" 
          - name: AUTOCLUSTER_CLEANUP
            value: "true"
          - name: CLEANUP_WARN_ONLY
            value: "false"
  volumeClaimTemplates:
  - metadata:
      name: rabbitmq-pvc
    spec:
      storageClassName: standard
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 20Gi

```

解释一下上面的描述文件:
* 创建了Headless Service用于暴露管理界面的Web端口31672。这里主要是为了演示,在实际应用中,这个可能是没必要的。
* 创建了rabbitmq的StatefulSet,含有3个节点,其中每个节点通过livenessProbe和readinessProbe检查可用性。 
* 使用volumeClaimTemplates自动生成volumeClaim,这里的template即模板,会自动给StatefulSet中的每一个节点创建一个Volume Claim,命名为rabbitmq-pvc-0/1/2 

下面我们来应用下上面的描述:
```
kubectl apply -f ./rabbitmq-service.yaml

service "rabbitmq" created
statefulset.apps "rabbitmq" created

```

稍过一会后,我们来看一下Web服务器的管理界面,http://192.168.99.100,用户名密码都是guest:

![查看RabbitMQ集群](./rabbitmq-cluster-status.png "查看RabbitMQ集群")

登录成功后,如上图所示。不难发现,已经成功启动了3个节点,并组成了集群,至此,我们的RabbitMQ基本集群配置成功。

## 配置镜像集群

下面,我们来配置镜像策略,在Web管理工具上点击"Admin" -> "Policies" -> "Add / update a policy",如下填写:
* Name: ha_mirror_queue
* Pattern: ^ 
* Apply to: All exchange and queues
* Defination: 
 * ha-mode:  all
 
添加好后点击"Add"

配置好后,我们尝试创建一个Queue,在Web管理工具点击"Queues" -> "Add queue",name写test,其他保持默认,最后点击"Add queue"。

添加完成后,可以发现队列有一个"+2"的标志,如下图所示。这个+2意思是队列有额外的2个备份(主1+镜2一共3个节点),镜像配置成功。

![查看RabbitMQ镜像队列](./rabbitmq-mirror-queue.png "查看RabbitMQ镜像队列")

至此,我们已经完成了RabbitMQ的集群配置、镜像队列配置。

关于Rabbit MQ的高可用、集群的更相信信息,可以查看官方文档:[RabbitMQ 集群](http://www.rabbitmq.com/clustering.html)。


================================================
FILE: legacy/ms-msgq/rocketmq-devops.md
================================================
# RocketMQ 消息队列的运维

在前两节,我们讨论了RabbitMQ。

然而根据实际的生产经验来看,当系统瞬时流量达到一定规模时,上述两款产品都不再适合作为消息系统的首选。

RabbitMQ在企业级应用是没有问题的,但它的抗消息堆积能力非常差,一旦突发流量提高,发生事件堆积,整个RabbitMQ集群就会挂起拒绝接受新消息,在极端情况下,甚至会发生RabbitMQ集群的宕机和消息丢失。

RocketMQ是一款高性能的分布式消息队列,它借鉴了Kafka的设计思路并继承了其高吞吐的特点,并重点改进了延迟,使得其更加适用于消息的实时处理。根据官方评测,在主流服务器上,RocketMQ的处理性能可达7万/秒,且随着Topic数量(队列数目)的增长而基本保持稳定,比Kafka更为稳定[^1]。

在本小节,我们将讨论RocketMQ的运维,一般有两种方案。

* RocketMQ部署在多台物理机上,优点是性能可靠。可以参考[官方集群部署文档](https://rocketmq.apache.org/docs/rmq-deployment/)。
* RocketMQ部署在容器集群上,优点是运维方便。本小节将主要介绍这种方案。

RocketMQ服务器有两种角色:
* NameServer: 管理元数据和Broker服务器、客户端的连接入口
* Broker: 处理消息队列的服务器

在本小节,我们将构建4台服务器的RocketMQ集群,2台NameServer、2台Broker。

首先构建4个PersistentVolume,给上述4台机器使用:
```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv011 
spec:
  storageClassName: standard
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 20Gi
  hostPath:
    path: /data/pv011/
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv012 
spec:
  storageClassName: standard
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 20Gi
  hostPath:
    path: /data/pv012/
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv021
spec:
  storageClassName: standard
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 20Gi
  hostPath:
    path: /data/pv021/
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv022
spec:
  storageClassName: standard
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 20Gi
  hostPath:
    path: /data/pv022/

```

应用4个持久化目录:
```
kubectl -f ./pvs.yaml
```

接着,看一下NameServer的部署:
```yaml
apiVersion: v1
kind: Service
metadata:
  name: rn 
spec:
  ports:
  - port: 9876
  selector:
    app: rocketmq-nameserver
  clusterIP: None
---
apiVersion: apps/v1
kind: StatefulSet 
metadata:
  name: rocketmq-nameserver 
spec:
  selector:
    matchLabels:
      app: rocketmq-nameserver
  serviceName: "rn"
  replicas: 2
  template:
    metadata:
      labels:
        app: rocketmq-nameserver 
    spec:
      restartPolicy: Always
      hostname: rocketmq-nameserver 
      containers:
      - name: rocketmq-nameserver-ct
        imagePullPolicy: Never
        image: coder4/rocketmq:4.2.0 
        ports:
        - containerPort: 9876 
        volumeMounts:
        - mountPath: /opt/rocketmq_home
          name: rocketmq-nameserver-pvc
        env:
        - name: NAME_SERVER
          value: "true"
  volumeClaimTemplates:
  - metadata:
      name: rocketmq-nameserver-pvc
    spec:
      storageClassName: standard
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 20Gi

```

如上所示:
* 我们使用了定制镜像coder4/rocketmq,它集成了NameServer/Broker并支持集群部署
* 使用StatefulSet部署两台相互独立的NameServer,主机名分别为rocketmq-nameserver-0和rocketmq-nameserver-1
* Volume的挂在点是/opt/rocketmq_home,其中会包含data和log两个子目录。

启动一下,稍后成功:
```yaml
kubectl apply -f ./nameserver-service.yaml

kubectl get pods

NAME                                                   READY     STATUS    RESTARTS   AGE
rocketmq-nameserver-0                                  1/1       Running   0          2m
rocketmq-nameserver-1                                  1/1       Running   0          2m

```

接下来,我们看一下Broker。
针对Broker,官方提供了几种高可用方案,我们这里采用"two master no slave"的模式,更多模式可参考官方文档。
```yaml
piVersion: v1
kind: Service
metadata:
  name: rb 
spec:
  ports:
  - name: p9
    port: 10909 
  - name: p11
    port: 10911
  selector:
    app: rocketmq-brokerserver
  clusterIP: None
---
apiVersion: apps/v1
kind: StatefulSet 
metadata:
  name: rocketmq-brokerserver 
spec:
  selector:
    matchLabels:
      app: rocketmq-brokerserver
  serviceName: "rb"
  replicas: 2
  template:
    metadata:
      labels:
        app: rocketmq-brokerserver 
    spec:
      restartPolicy: Always
      hostname: rocketmq-brokerserver 
      containers:
      - name: rocketmq-brokerserver-ct
        imagePullPolicy: Never
        image: rocketmq:latest
        ports:
        - containerPort: 10909 
        - containerPort: 10911
        volumeMounts:
        - mountPath: /opt/rocketmq_home
          name: rocketmq-brokerserver-pvc
        env:
        - name: NAME_SERVER_LIST
          value: "rocketmq-nameserver-0.rn:9876;rocketmq-nameserver-1.rn:9876" 
        - name: BROKER_SERVER
          value: "true"
        - name: BROKER_CLUSTER_CONF
          value: "2m-noslave"
  volumeClaimTemplates:
  - metadata:
      name: rocketmq-brokerserver-pvc
    spec:
      storageClassName: standard
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 20Gi

```

上述brokerserver的配置与nameserver存在如下区别:
* 通过环境变量NAME_SERVER_LIST设定了nameServer的集群列表,即之前启动的两台机器。
* BROKER_SERVER表示启用的是broker server模式
* BROKER_CLUSTER_CONF表示集群配置模式,即我们提到的"双主零从模式"

我们也启动一下broker server,稍等一会后,会成功:
```yaml
kubectl apply -f ./broker-service.yaml

kubectl get pods

NAME                                                   READY     STATUS    RESTARTS   AGE
rocketmq-brokerserver-0                                1/1       Running   0          59s
rocketmq-brokerserver-1                                0/1       Running   0          49s

```

我们尝试用RocketMQ的自带工具n查看一下broker集群状态,能发现两台机器,说明集群部署成功:
```shell
./mqadmin clusterList -n "rocketmq-nameserver-0.rn:9876;rocketmq-nameserver-1.rn:9876"        

#Cluster Name     #Broker Name            #BID  #Addr                  #Version                #InTPS(LOAD)       #OutTPS(LOAD) #PCWait(ms) #Hour #SPACE
DefaultCluster    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
DefaultCluster    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

```

至此,我们完成了RocketMQ的集群部署工作。

[^1]: [Kafka vs. RocketMQ- Multiple Topic Stress Test Results](https://medium.com/@Alibaba_Cloud/kafka-vs-rocketmq-multiple-topic-stress-test-results-d27b8cbb360f)



================================================
FILE: legacy/ms-msgq/sb-kafka.md
================================================
# Spring Boot整合Kafka



================================================
FILE: legacy/ms-msgq/sb-rabitmq.md
================================================
# Spring Boot整合RabbitMQ

在上一节,我们已经掌握了RabbitMQ集群的运维方法。

在本章中,我们来看一下如何在Spring Boot中集成RabbitMQ

## 依赖配置
RabbitMQ实现了AMQP协议,因此,在Spring Boot中,我们直接引入ampq的starter
```groovy
compile("org.springframework.boot:spring-boot-starter-amqp")
```

## RabbitMQ服务配置
在yaml中,我们需要配置RabbitMQ服务的地址:
```yaml
spring.rabbitmq:
  addresses: rmq1:5672,rmq2:5672
  username: guest
  password: guest
```
## 发送消息
在Spring Boot中发送消息需要4个步骤:
1. 声明Exchange
1. 声明Queue
1. 声明Queue到Exchange的绑定
1. 调用RabbitTemplate发送消息

前三步声明都可以通过Spring Boot注入:
```java
#Bean
public TopicExchange createExchange() {
    return new TopicExchange("exchange");
}
 
@Bean
public Queue createQueue() {
    return new Queue("queue");
}
 
 
@Bean
public Binding declareBindingGeneric() {
    return BindingBuilder.bind(createQueue()).to(createExchange()).with("#");
}

```

如上所示,分别声明了Exchange、Queue和Binding

发送消息相对比较简单,拿到注入的RabbitTemplate后,直接发送即可。

```java
public class Tut1Sender {

    @Autowired
    private RabbitTemplate template;

    @Autowired
    private Queue queue;

    @Scheduled(fixedDelay = 1000, initialDelay = 500)
    public void send() {
        String message = "Hello World!";
        this.template.convertAndSend(queue.getName(), message);
        System.out.println(" [x] Sent '" + message + "'");
    }
}


```

如上,我们直接调用convertAndSend即可发送字符串类型的消息。如果想发送更复杂类型的,可以让类型实现Converter。

## 接受消息

接受消息,直接绑定Queue的名字即可:
```java
@RabbitListener(queues = "queue")
public class Tut1Receiver {

    @RabbitHandler
    public void receive(String in) {
        System.out.println(" [x] Received '" + in + "'");
    }
}
```

至此,我们可以在Sping Boot中集成RabbitMQ了。


================================================
FILE: legacy/ms-msgq/sb-rocketmq.md
================================================
# Spring Boot整合RocketMQ

在本小节中,我们将讨论在Spring Boot中整合RocketMQ。

我们选用官方推荐的Spring Boot拓展[rocketmq-spring-boot-starter](https://github.com/apache/rocketmq-externals/tree/master/rocketmq-spring-boot-starter)

## 依赖接入

首先,需要在Spring Boot中添加依赖:
```groovy
compile 'com.qianmi:spring-boot-starter-rocketmq:1.1.0-RELEASE'
```

## 配置RocketMQ的服务器

在配置中,我们需要设定RocketMQ的服务器集群

```yaml
spring.rocketmq:
    nameServer: 127.0.0.1:9876
    producer.group: lmsia-abc
```

如上所示:
* spring.rocketmq.nameServer 配置元服务器的地址
* spring.rocketmq.producer.group 是[Producer Group](http://rocketmq.apache.org/docs/core-concept/)。我们这里设定为微服务的名字,即相同的微服务都认为是同一个Producer Group。

## 在Spring Boot中首发消息

按照上述进行配置后,自动配置会被激活,并自动注入RocketMQTemplate用于生成发消息、ListenerContainer用于收消息。

上述对使用方都是透明的,在Spring Boot中收发消息非常简单,如下:

```java
@Service
@RocketMQMessageListener(topic = TOCPIC, consumerGroup = LmsiaAbcConstant.PROJECT_NAME)
public class MyEventHandlerImpl implements MyEventHandler, RocketMQListener<MyEvent> {

    private Logger LOG = LoggerFactory.getLogger(getClass());

    @Resource
    private RocketMQTemplate rocketMQTemplate;

    @Override
    public void send(MyEvent event) {
        rocketMQTemplate.convertAndSend(TOCPIC, event);
    }

    @Override
    public void onMessage(MyEvent message) {
        LOG.info("receive message, data = {}", message.getData());
    }
}
```

如上所述:
* topic需要配置成一致的,即TOPIC常量,类似的,consumerGroup定义为微服务名称,即认为同一个微服务的所有节点属于同一个组。
* 我们自动注入了RocketMQTemplate用于发消息,消息默认继承自Serializable
* RocketMQListener会在收到消息后回调onMessage方法。

如果你使用过RocketMQ的官方客户端的话,会发现其易用性要远低于[spring-boot-starter-rocketmq](https://github.com/apache/rocketmq-externals/tree/master/rocketmq-spring-boot-starter)。

RocketMQ还支持顺序消息、广播消息等更多功能,spring-boot-starter-rocketmq中都支持了具体的实现,可以直接参考上述项目主页的说明。


================================================
FILE: legacy/ms-storage/README.md
================================================
# 微服务的存储与缓存

在计算机领域,有一句人尽皆知的名言"算法=程序+数据结构",这是图灵奖获得者尼古拉斯·沃斯提出的[^1]。

从大师的这句名言中,我们不难感受到,数据的存储方式与算法同等重要。

在微服务架构中,我们虽然不会研究特定的算法,但数据的存储依然是必不可少的环节。

例如:用户的注册信息、订单信息、生成的UGC内容等,都需要以合适的方式存储下来。

存下来只是第一步,更为关键的是,我们需要在需要时,以合理的速度、合理的成本开销将数据读取出来。特别是在互联网软件开发中,数据经常是"读多写少",数据的读取往往比存储更加关键。

请注意我的用词"合理的速度"、"合理的成本"。为了理解这两点,我们先了解一下常见的存储方式:




请注意我的用词,是"合理的速度"、"合理的成本",并非"最快的速度"、"最低的成本"。






[^1](https://www.jianshu.com/p/19f68c72effc)



================================================
FILE: legacy/ms-storage/memcached-devops.md
================================================
# Memcached 缓存服务的运维 

如果业务进一步发展,通过"读写分离"、"分库分表"后,数据库的性能依然无法满足高并发读请求,此时就需要缓存出马了。

缓存的原理其实非常简单: 用"存取速度更快的空间"换取"存取速度更慢的时间"。

当然,天下没有免费的午餐,缓存自然也是有代价的。单位容量的内存比磁盘要昂的多。幸运的是,根据数据的局部性原理[^2],我们可以有如下策略:
* 只选择少量的"热数据"放入缓存
* 当缓存空间不够放置"热数据"时,根据策略替换掉缓存中的已有数据。

本节的主角是Memcached,一款高性能的内存缓存,性能可达每秒5万次[^1]。

Memcached本身是不支持集群的,但可以通常可以部署多台服务。在访问时,可以根据key的哈希值取模进行分片,然后访问不同的Memcached结点。

## 集群搭建
由于Memcached是全内存的,所以无需创建Volume挂载点。

在这里,我们没有使用Deployment,而是使用了[StatefulSet](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/)。

StatefulSet与Deployment基本相同,唯一的的区别是,前者认为所有副本是相互独立的,而后者认为所有副本是互为冗余的。

对于微服务的应用场景,每个节点都是完全相同的逻辑、连接完全相同的数据库、执行等同的操作,所以我们用的一直是Deployment。

而对于Memcached,我们会将不同数据分片到不同Memcached结点上,他们是相互独立而不是可替代的,所以我们采用了StatefulSet。

memcached-service.yaml:
```yaml
apiVersion: v1
kind: Service
metadata:
  name: memcached
spec:
  ports:
  - port: 11211
  selector:
    app: memcached
  clusterIP: None
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: memcached
spec:
  selector:
    matchLabels:
      app: memcached
  serviceName: "memcached"
  replicas: 2
  template:
    metadata:
      labels:
        app: memcached
    spec:
      restartPolicy: Always
      hostname: memcached
      containers:
      - name: memcached-ct
        image: memcached:1.5-alpine
        ports:
        - containerPort: 11211
        args: ["memcached", "-m", "256"]

```

简单说明下:
* 我们声明了StatefulSet为memcached,2个独立节点
* 限定了内存使用为256m

启动下:
```shell
kubectl apply -f memcached-service.yaml
```

然后我们登录一个额外的docker上,尝试ping一下,是可以的,说明启动成功:
```shell

ping memcached-0.memcached
PING memcached1 (172.17.0.8): 56 data bytes
64 bytes from 172.17.0.8: seq=0 ttl=64 time=1.014 ms
64 bytes from 172.17.0.8: seq=1 ttl=64 time=0.138 ms
64 bytes from 172.17.0.8: seq=2 ttl=64 time=0.134 ms


ping memcached-1.memcached
PING memcached2 (172.17.0.9): 56 data bytes
64 bytes from 172.17.0.9: seq=0 ttl=64 time=0.076 ms
64 bytes from 172.17.0.9: seq=1 ttl=64 time=0.123 ms
64 bytes from 172.17.0.9: seq=2 ttl=64 time=0.119 ms

```

注意上面对不同节点的DNS域名为"statefulName-x"."serviceName"

Memcached的配置看起来很简单,但是分片策略还需要进一步思考。

例如,前面提到了利用哈希值取模,可以实现Memcahced在客户端的分片。按照此策略,如果现在要增加一台机器到3台,计算取模的值将发生变化,缓存上的所有的数据都需要清空重新分片。

这种"推倒重来"的策略看起来简单,但可能会导致缓存击穿甚至造成线上故障。

想解决这类问题,可以采用[一致性哈系](http://www.cnblogs.com/RockLi/p/3530176.html),它可以尽可能地减少机器变动后,造成的数据重分布。

Memcached的日常运维比较简单,常见的操作就是清空全部缓存,可以通过nc指令来完成:

```shell
echo 'flush_all' | nc memcached1 11211

OK
```

提醒一下,线上执行清空操作要非常谨慎,若系统性能严重依赖缓存,那么清空操作往往会导致缓存击穿并造成系统故障。

## 小结

本节,我们使用StatefulSet,完成了Memcached集群的运维,并介绍了Memcahced集群运维中常见的问题。

[^1]: [Memcached性能评测数据](https://github.com/scylladb/seastar/wiki/Memcached-Benchmark)
[^2]: 分为空间局部性和时间局部性,可参考[局部性原理浅析](https://www.cnblogs.com/yanlingyin/archive/2012/02/11/2347116.html)


================================================
FILE: legacy/ms-storage/mysql-devops.md
================================================
# MySQL数据库的运维

近几年,以"Redis"、"MongoDB"为代表的"NoSQL"数据库迅速崛起。"NoSQL"并不是"没有SQL"而是"Not Only SQL"。在特定场景下,NoSQL数据库确实解决了一些问题,例如:

* 海量数据的列存储: 以HBase、Cassandra。
* 速度更快的内存数据库:Redis。
* 文档存储:Mongo DB。
* 支持全文检索特性: ElasticSearch。

若将视角放到更普适的场景,关系型数据库凭借着"ACID"特性,依然牢牢占据着数据存储的核心地位。

根据市场调研显示,在数据库领域,排名前3的分别是:
1. Oracle
1. MySQL
1. Microsoft SQL Server

其中,MySQL是最流行的开源关系数据库,其性能较为优秀、服务稳定、社区活跃,是众多中小型公司的首选。

在本节中,我们首先将讨论MySQL数据库的基础运维,随后讨论常见的优化手段-MySQL主从复制。

## MySQL数据库的部署

我们的MySQL依然部署在Kubernetes上,首先创建挂载点:
```shell
sudo mkdir /data/mysql

sudo chmod -R 777 /data/mysql

```

如果是线上环境,可以将挂载点设定在SSD磁盘等IOPS较高的存储介质上。

看一下部署脚本:
```yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql
spec:
  ports:
  - port: 3306
  selector:
    app: mysql
  clusterIP: None
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql
spec:
  selector:
    matchLabels:
      app: mysql
  replicas: 1
  template:
    metadata:
      labels:
        app: mysql
    spec:
      nodeSelector:
        kubernetes.io/hostname: minikube
      restartPolicy: Always
      hostname: mysql
      containers:
      - name: mysql-ct
        image: mysql:8
        ports:
        - containerPort: 3306
        volumeMounts:
        - mountPath: "/var/lib/mysql"
          name: volume
        env:
        - name: "MYSQL_ROOT_PASSWORD"
          value: "root123"
      volumes:
      - name: volume
        hostPath:
          path: /data/mysql/

```

上面的部署yaml与之前的类似,只解释几点:
* 上述实际启动了一个"Headless"Service,即将clusterIP设置为None。此时将不再启动单独的VIP,而是让dns直接解析到Pod的IP上
* MySQL的存储数据量较大,一般固定在某台物理机上,不会主动迁移。例如这里我们选择了minikube。
* MYSQL_ROOT_PASSWORD是root的管理员密码,但root默认只允许本机登录。

我们来尝试登录下,首先获取docker的容器id:
```shell
kubectl get pods

NAME                                                READY     STATUS    RESTARTS   AGE
mysql-7bf88bcfd-gljv7                               1/1       Running   1          4m

kubectl describe pod mysql-7bf88bcfd-gljv7 

...
Container ID:   docker://479a3b1d9e7b9f445f2cb8133e156de480337c23c6f27aa541ca4df8b3cf944d
...
```

然后尝试登录MySQL,是成功的:
```shell
minikube ssh

$docker exec -i -t 479a3b1d9e7b9f445f2cb8133e156de480337c23c6f27aa541ca4df8b3cf944d /bin/sh

$#mysql -h localhost -u root -proot123

mysql -h localhost -uroot -proot123

mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.11 MySQL Community Server - GPL

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> 

```

至此,我们已经完成了MySQL数据库的基本搭建。

## MySQL数据库的日常运维

出于性能、安全性、可拓展性等考量,一般会为MySQL服务器建立多个库,并分配不同的帐号。在微服务架构下,不同的微服务应使用不同的库,尽量避免数据库层的耦合。

因此,新建数据库、分配帐号是MySQL日常运维工作最常见的事情。我们给出一个脚本:

```shell
if [ $# -ne 1 ]; then
    echo "Usage $0 <dbname>"
    exit -1
fi

DB_NAME=$1
DB_USER="lmsia"
DB_PASS="pass"

echo "CREATE DATABASE IF NOT EXISTS $DB_NAME DEFAULT CHARSET utf8mb4;"
echo "CREATE USER $DB_USER IDENTIFIED by '$DB_PASS';"
echo "GRANT ALL PRIVILEGES ON $DB_NAME.* to $DB_USER;"
echo "FLUSH PRIVILEGES;"

```

在上述脚本中,会自动生成如下语句:
* 建库(编码utf8mb4)
* 建用户,并分配到上述库

注意:脚本里的密码给的是固定的"pass",在实际生产环境,应当使用随机密码。

我们可以执行上述脚本,然后将生成的语句粘帖到root登录的mysql客户端,即可完成数据库的添加工作。

## MySQL数据库的同步

MySQL较为优秀,但也不是万能的银弹。随着业务逐步发展壮大,MySQL的性能可能会成为瓶颈,例如:
* CPU占用持续较高
* 网络带宽常常打满
* 查询慢

当性能成为瓶颈时,应首先从使用方面查找原因,举几个常见例子:
* 查询语句是否合理利用了索引
* 事务锁表时间是否太长
* 是否经常性的超大批量数据导出占用了带宽

如果使用上都没有问题,依然出现性能问题,可以考虑从架构上优化,常见的手段有:
* 读写分离: 如果读多写少,可以MySQL端进行主从复制,微服务中读写分离。
* 分库: 某个库占用大量性能,可考虑将其拆分到单独的MySQL服务器上
* 分表: 单表行数超过1000万后,可考虑将表进行水平、垂直划分,拆分到不同MySQL服务器上。

其中分库和分表一般要借助中间件实现,在本小节,我们先介绍第一种手段:读写分离。

读写分离也是一种基于特定场景的优化。在互联网软件开发中,经常会有"读多写少"的情景,举几个例子:
* 微博上看的人多,发微博的人少。
* 新闻浏览的人数多,评论的人数少,发布的新闻更少。

针对"读多写少",我们可以将读和写分离开,使得读性能得到更高的保证,看一下读写分离的原理:

![MySQL读写分离](./mysql-replication.png "MySQL读写分离")

如图所示,我们开启两个MySQL服务器:
* Master,MySQL数据库服务器,主要承担写请求
* Slave,MySQL数据库服务器,负责承担读请求

要说明的是:写SQL到达Master后,会通过住从复制机制(Replication)同步到Slave上,这个过程是异步的,会存在延迟。若微服务的读请求都发往Slave,那么势必也会受到住从复制的延迟影响,特别是对于"写后读"这个场景,可能会导致一些Bug,感兴趣的朋友自行思考如何解决。

MySQL的主从数据同步有几个要点:
* master和slave机器配置了不同的server-id
* slave机器通过配置或sql命令设定master的主机名

下面我们尝试在Kubernetes中配置一组主从复制的MySQL服务器。

首先创建master(写库)和slave(读库)的挂载点:
```shell
sudo mkdir /data/mysql_writer
sudo mkdir /data/mysql_reader

sudo chmod -R 777 /data/mysql_writer
sudo chmod -R 777 /data/mysql_reader
```

然后看一下master(写服务)的定义, mysql-writer-service.yaml:
```yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql-writer
spec:
  ports:
  - port: 3306
  selector:
    app: mysql-writer
  clusterIP: None
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql-writer
spec:
  selector:
    matchLabels:
      app: mysql-writer
  template:
    metadata:
      labels:
        app: mysql-writer
    spec:
      nodeSelector:
        kubernetes.io/hostname: minikube
      restartPolicy: Always
      hostname: mysql-writer
      containers:
      - name: mysql-writer-ct
        image: coder4/mysql-replication:8.0
        ports:
        - containerPort: 3306
        volumeMounts:
        - mountPath: "/var/lib/mysql"
          name: volume
        env:
        - name: "MYSQL_ROOT_PASSWORD"
          value: "root123"
        args: ["--server-id=1"]
      volumes:
      - name: volume
        hostPath:
          path: /data/mysql_writer/
````

如上所述,配置基本与之前的单机MySQL类似,区别是:
* 使用了一个支持主从同步的镜像,coder4/mysql-replication
* 主机名、dns配置为mysql-writer
* 服务id是1

应用一下,过一会可以看到启动成功:
```shell
kubectl apply -f ./mysql-writer-service.yaml
```

下面看下读库(slave结点), mysql-reader-service.yaml:
```yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql-reader
spec:
  ports:
  - port: 3306
  selector:
    app: mysql-reader
  clusterIP: None
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql-reader
spec:
  selector:
    matchLabels:
      app: mysql-reader
  template:
    metadata:
      labels:
        app: mysql-reader
    spec:
      nodeSelector:
        kubernetes.io/hostname: minikube
      restartPolicy: Always
      hostname: mysql-reader
      containers:
      - name: mysql-reader-ct
        image: coder4/mysql-replication:8.0
        ports:
        - containerPort: 3306
        volumeMounts:
        - mountPath: "/var/lib/mysql"
          name: volume
        env:
        - name: "MYSQL_ROOT_PASSWORD"
          value: "root123"
        - name: "MYSQL_MASTER_SERVER"
          value: "mysql-writer"
        args: ["--read-only=1", "--server-id=2"]
      volumes:
      - name: volume
        hostPath:
          path: /data/mysql_reader/
```

与独立MySQL服务器的配置相比,主要做了如下修改:
* 设置主结点(写库)为mysql-writer
* 用于主从同步的用户名密码同写库
* 只读,这主要是为了防止误删除数据,导致主从不一致
* 服务id是2

类似的,我们启动下从库:
```shell
kubectl apply -f ./mysql-reader-service.yaml
```

下面我们尝试在主库创建数据库,看看能否自动同步到从库:

(这里省略登录mysql-writer的过程,具体参照本节第一部分)
```sql
CREATE DATABASE IF NOT EXISTS lmsia_user DEFAULT CHARSET utf8mb4;
CREATE USER lmsia IDENTIFIED by 'pass';
GRANT ALL PRIVILEGES ON lmsia_user.* to lmsia;
FLUSH PRIVILEGES;
```

然后登录mysql-reader看一下,发现可以成功登录,说明主从同步的配置是成功的:
```shell
mysql -hlocalhost -ulmsia -ppass lmsia_user
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 6
Server version: 5.7.14-log MySQL Community Server (GPL)

Copyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

```

上述例子只展示了coder4/mysql-replication镜像的部分配置,如果你想启用更多高级配置,可以参考[docker-mysql-replication](https://github.com/liheyuan/docker-mysql-replication)

MySQL主从同步是一个很有意思的话题,例如:
* 上述例子中,mysql-reader只会同步启动后接受到的binlog,如何同步启动前mysql-writer的改动呢?
* 本小节一开始提到的"写后读"问题,能不能通过"写到slave后才算写完成"的方式解决呢?
* 能否配置多个slave节点呢?

由于篇幅所限,这里不会对上述问题展开讨论,如果你感兴趣,可以在[MySQL Replication官方文档](https://dev.mysql.com/doc/refman/8.0/en/replication.html)中找到答案。



================================================
FILE: legacy/ms-storage/redis-devops.md
================================================
# Redis 数据库的运维

作为纯内存缓存,Memcached拥有非常出色的读写性能,但也存在一个较为严重的缺点:无法持久化。

这意味着,一旦Memcached服务重启(更常见的是掉电),之前所有的缓存就会丢失。若线上的流量很大,这种重启很容易诱发"缓存雪崩",从而导致系统故障。

Redis的出现很好的解决了这个问题,它是一款高性能的内存的数据库,既不仅数据的支持持久化、也内置了许多数据结构,方便实现各种需求。在一些场景下[^1],可以直接用Redis取代Memcached + MySQL的组合。

本节将讨论Redis运维相关的问题。

## Redis单服务器的运维

Redis同时支持单服务器、高可用、集群等三种方案。

我们先来看一下单服务器方案。

顾名思义,单服务器模式下,只启动一个Redis服务进程,若服务挂掉则Redis不可用。可见,这种方案并不保证高可用。

与之前的部署类似,我们同样将Redis部署在Kubernetes集群上,首先是创建Volume挂载点

```shell
sudo mkdir /data/redis

sudo chmod -R 777 /data/redis

```

接着,我们看一下部署文件:
```yaml
apiVersion: v1
kind: Service
metadata:
  name: redis
spec:
  ports:
  - port: 6379
  selector:
    app: redis
  clusterIP: None
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
spec:
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      nodeSelector:
        kubernetes.io/hostname: minikube
      restartPolicy: Always
      hostname: redis
      containers:
      - name: redis-ct
        image: redis:3.2-alpine
        ports:
        - containerPort: 6379
          hostPort: 6379
        volumeMounts:
        - mountPath: "/data"
          name: volume
      volumes:
      - name: volume
        hostPath:
          path: /data/redis/

```

简要说明下:
* 这里使用Redis官方的Docker镜像
* 与MySQL类似,考虑到持久化后的数据量可能较大,我们将Pod绑定到minikube机器上,以固定存储。

应用servie,稍等一会,成功:
```yaml
kubectl apply -f kubectl describe pod redis-798659bc79-vdht7

service "redis" created
deployment.apps "redis" created

```

我们尝试连接一下,首先获取Pod的ContainerId:
```shell

kubectl get pods
NAME                                                READY     STATUS    RESTARTS   AGE
redis-798659bc79-vdht7                              1/1       Running   0          4m

kubectl describe pod redis-798659bc79-vdht7

...
    Container ID:   docker://090c2a7a004200aa6f0c4f3779e3823c401f03ad4f23985fdc08c38f86d6c598
...


```

尝试登录,并登录redis服务器:
```shell
minikube ssh
$ docker exec -i -t 090c2a7a004200aa6f0c4f3779e3823c401f03ad4f23985fdc08c38f86d6c598 /bin/sh

/data # echo "info" | redis-cli 
# Server
redis_version:3.2.12
redis_git_sha1:00000000
....

```

通过上面的操作,可以成功登录Redis服务器,并获取了版本信息。

至此,Redis的单服务器模式配置完成。

## Redis高可用(Sentinel)集群的运维

在上面的Redis单服务器模式下,存在单点故障,假如这个Redis进程挂掉了,则Redis就无法提供服务了。

为了解决可用性,Redis内置了两种高可用方案,较为经典的是Sentinel集群。
Sentinel集群采用主备模式:
* 支持多个Redis服务组,不同服务组通过唯一的master_name标识。
* 组内一个主redis节点提供服务,若干从redis节点定期从主redis节点同步数据。但从节点只作为热备,不提供服务。
* 当某个组的主节点挂掉后,Sentinel服务会检测到主节点故障,并进行主备切换。
* 客户端先连接Sentinel,根据master_name获取组内主Redis节点的IP和端口信息,再连接。

![Sentinel集群架构](./redis-sentinel.png "Sentinel架构")

如果你对Sentinel的架构细节感兴趣,可以阅读[官方文档](https://redis.io/topics/sentinel)。

首先,我们来部署一组Redis服务的主节点:
```yaml
apiVersion: v1
kind: Service
metadata:
  name: redis-lmsia-test1-master
spec:
  ports:
  - port: 6379
  selector:
    app: redis-lmsia-test1-master
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-lmaia-test1-deployment
spec:
  selector:
    matchLabels:
      app: redis-lmsia-test1-master
  template:
    metadata:
      labels:
        app: redis-lmsia-test1-master
    spec:
      nodeSelector:
        kubernetes.io/hostname: minikube
      restartPolicy: Always
      hostname: redis
      containers:
      - name: redis-sentinel-ct
        image: coder4/redis-sentinel-k8s:4.0.10
        ports:
        - containerPort: 6379
        env:
        - name: "MASTER"
          value: "true"
        - name: "MASTER_NAME"
          value: "lmsia_test1"
```

如上所示:
* 我们使用了自定制的镜像redis-sentinel-k8s,其原理可以查看项目主页[docker-redis-sentinel-k8s](https://github.com/liheyuan/docker-redis-sentinel-k8s)
* MASTER=true,开启主节点模式
* MASTER_NAME=lmsia_test1,Redis服务的组名叫lmsia_test1
* 服务组名是redis-lmsia-test1-master,这个很重要,slave节点和sentinel会根据这个来定位master节点。

接着,我们启动lmsia_test1这个服务组一个从节点:
```yaml
apiVersion: v1
kind: Service
metadata:
  name: redis-lmsia-test1-slave
spec:
  ports:
  - port: 6379
  selector:
    app: redis-lmsia-test1-slave
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-lmaia-test1-deployment
spec:
  selector:
    matchLabels:
      app: redis-lmsia-test1-slave
  template:
    metadata:
      labels:
        app: redis-lmsia-test1-slave
    spec:
      nodeSelector:
        kubernetes.io/hostname: minikube
      restartPolicy: Always
      hostname: redis
      containers:
      - name: redis-sentinel-ct
        image: coder4/redis-sentinel-k8s:4.0.10
        ports:
        - containerPort: 6379
        env:
        - name: "SLAVE"
          value: "true"
        - name: "MASTER_NAME"
          value: "lmsia_test1"
```

如上,组内从节点的启动方式和主节点基本一致,有几个需要特别注意的:
* MASTER_NAME需要和主节点保持一致,即lmsia_test1
* SLAVE=true,开启从节点模式。

我们先来启动这一组主从服务:
```shell
kubectl apply -f ./redis-lmsia-test1-master-service.yaml
kubectl apply -f ./redis-lmsia-test1-slave-service.yaml
```

我们分别登录redis,看看他们的组状态,首先是master,身份是主节点,并可以看到从节点的IP:
```shell
redis-cli>info replication
info replication
# Replication
role:master
connected_slaves:1
slave0:ip=172.17.0.8,port=6379,state=online,offset=168,lag=1
master_replid:9b7dfe0b5f8d538d0f7b81d4095c239f1da72553
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:168
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:168

```

然后slave,状态是从节点,可以看到主节点的IP:
```shell
# Replication
role:slave
master_host:10.105.12.178
master_port:6379
master_link_status:up
master_last
Download .txt
gitextract_6y7hg2hl/

├── .gitignore
├── README.md
├── book.toml
├── legacy/
│   ├── SUMMARY.md
│   ├── architecture/
│   │   ├── README.md
│   │   ├── devops.md
│   │   ├── microservics.md
│   │   ├── ms-arch.xml
│   │   ├── overview.md
│   │   └── toolchain.md
│   ├── devops/
│   │   ├── README.md
│   │   ├── discovery.md
│   │   ├── docker-repo.md
│   │   ├── jump-server.md
│   │   └── openvpn-k8s.md
│   ├── k8s/
│   │   ├── README.md
│   │   ├── docker-k8s.md
│   │   ├── helm.md
│   │   ├── k8s-cluster.md
│   │   ├── k8s-ha.md
│   │   ├── k8s-intro.md
│   │   ├── k8s-ipvs.md
│   │   └── k8s-office.md
│   ├── ms-circuit-breaker-and-limit/
│   │   ├── README.md
│   │   ├── sb-hystrix.md
│   │   └── sb-limit.md
│   ├── ms-config/
│   │   ├── README.md
│   │   ├── cfg4j.md
│   │   ├── consul-devops.md
│   │   └── sb-config.md
│   ├── ms-delivery/
│   │   ├── README.md
│   │   ├── jenkins-devops.md
│   │   ├── ms-cd.md
│   │   └── ms-ci.md
│   ├── ms-discovery/
│   │   ├── README.md
│   │   ├── msd.md
│   │   └── service-discovery.xml
│   ├── ms-log/
│   │   ├── README.md
│   │   ├── elk-devops.md
│   │   ├── sb-eblk.md
│   │   ├── sb-logback.md
│   │   └── sb-trace.md
│   ├── ms-monitor/
│   │   ├── README.md
│   │   ├── k8s-prometheus-grafana.md
│   │   ├── sb-prometheus.md
│   │   ├── sb-sentry.md
│   │   ├── sentry-devops.md
│   │   └── sentry.txt
│   ├── ms-msgq/
│   │   ├── README.md
│   │   ├── dev-kafka.md
│   │   ├── kafka-devops.md
│   │   ├── rabbitmq-devops.md
│   │   ├── rocketmq-devops.md
│   │   ├── sb-kafka.md
│   │   ├── sb-rabitmq.md
│   │   └── sb-rocketmq.md
│   ├── ms-storage/
│   │   ├── README.md
│   │   ├── memcached-devops.md
│   │   ├── mysql-devops.md
│   │   ├── redis-devops.md
│   │   ├── sb-memcached.md
│   │   ├── sb-mysql.md
│   │   └── sb-redis.md
│   ├── spring-boot/
│   │   ├── README.md
│   │   ├── discovery.md
│   │   ├── gerrit.md
│   │   ├── graceful-shutdown.xml
│   │   ├── mockito.md
│   │   ├── rest-nginx.xml
│   │   ├── sb-gradle-structure.md
│   │   ├── sb-mockito.md
│   │   ├── sb-rest.md
│   │   └── sb-thrift.md
│   └── toolchain/
│       ├── README.md
│       ├── bom.md
│       ├── gerrit.md
│       ├── kanboard.md
│       ├── ldap.md
│       ├── nexus.md
│       ├── spring-boot-scripts.md
│       ├── spring-boot-template.md
│       └── stress-test.md
└── src/
    ├── README.md
    ├── SUMMARY.md
    ├── ch01-architecture/
    │   ├── README.md
    │   ├── continuous-x.md
    │   ├── micro-service-intro.md
    │   ├── ms-arch.plantuml
    │   ├── ms-architecture.md
    │   ├── ms-tech-stack.md
    │   └── rd-ops-toolchain.md
    ├── ch02-ms-dev1/
    │   ├── README.md
    │   ├── database1.md
    │   ├── database2.md
    │   ├── gradle.md
    │   ├── redis.md
    │   ├── rpc.md
    │   └── spring-boot.md
    ├── ch03-ms-dev2/
    │   ├── README.md
    │   ├── circuit-breaker-and-limiter.md
    │   ├── config.md
    │   ├── mq.md
    │   ├── registry1.md
    │   └── registry2.md
    ├── ch04-ms-dev3/
    │   ├── README.md
    │   ├── elkfk.md
    │   ├── micrometer.md
    │   ├── skywalking.md
    │   └── victorialmetrics.md
    ├── ch05-k8s/
    │   ├── README.md
    │   ├── container.md
    │   ├── k8s-101.md
    │   ├── k8s-cluster.md
    │   ├── k8s-ha-cluster.md
    │   └── k8s-ingress.md
    ├── ch06-cd/
    │   ├── README.md
    │   ├── jenkins-custom.md
    │   ├── jenkins-k8s-optimize.md
    │   ├── jenkins-k8s.md
    │   └── jenkins.md
    └── ch07-tools/
        ├── .md
        ├── README.md
        ├── gitlab.md
        ├── jfrog-artifactory.md
        ├── ldap.md
        ├── microservice-template.md
        ├── registry2.md
        └── seafile.md
Condensed preview — 128 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (812K chars).
[
  {
    "path": ".gitignore",
    "chars": 15,
    "preview": "book\n.DS_Store\n"
  },
  {
    "path": "README.md",
    "chars": 1748,
    "preview": "# 从0到1实战微服务架构(开源电子书)\n\n## 前言\n\n微服务是继SOA后,最流行的服务架构风格之一。\n\n按照微服务对系统进行拆分后,每个服务的业务逻辑都更加简单、清晰。服务之间是松耦合的,模块之间的边界也更加清晰。\n\n微服务有效降低了软"
  },
  {
    "path": "book.toml",
    "chars": 102,
    "preview": "[book]\nauthors = [\"lihy\"]\nlanguage = \"en\"\nmultilingual = false\nsrc = \"src\"\ntitle = \"从0到1实战微服务架构(第2版)\"\n"
  },
  {
    "path": "legacy/SUMMARY.md",
    "chars": 3166,
    "preview": "# Summary\n\n* [从0到1实战微服务架构](README.md)\n\n* [架构概览](architecture/README.md)\n    * [微服务架构概览](architecture/overview.md)\n    * "
  },
  {
    "path": "legacy/architecture/README.md",
    "chars": 158,
    "preview": "# 架构概览\n\n当我们谈微服务架构时,我们需要关注哪些点,本章将从这里说起。在了解了微服务的整体架构后,我们会从“研发工具链”、“微服务技术栈”、“运维技术链条”三个角度展开。我们会讨论微服务架构中,如何对这三类问题进行技术选型以及做出这些"
  },
  {
    "path": "legacy/architecture/devops.md",
    "chars": 1608,
    "preview": "# 运维工具链概览\n\n在看过微服务整体架构后,我们来讨论下架构的各个层次中,本书所选用的技术栈。\n\n与前面类似,我们依然自底向上讨论。\n\n运维工具链选型\n* 基础设施层:对于绝大多数的中小公司,且无强烈的数据保密需求,我强烈建议使用云主机。"
  },
  {
    "path": "legacy/architecture/microservics.md",
    "chars": 2000,
    "preview": "# 微服务技术栈概览\n\n下面来看一下微服务相关的技术选型\n\n* 服务开发框架:在微服务开发方面,我们选用Java作为开发语言。市面上的语言众多,特别近几年来,Go、Rust语言作为服务端语言快速崛起。既然如此,我们为什么还要选用基于Java"
  },
  {
    "path": "legacy/architecture/ms-arch.xml",
    "chars": 2224,
    "preview": "<mxfile userAgent=\"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:59.0) Gecko/20100101 Firefox/59.0\" version=\"8.6.0\" editor="
  },
  {
    "path": "legacy/architecture/overview.md",
    "chars": 3327,
    "preview": "# 微服务架构概览\n\n在正式讨论微服务架构前,有必要用简短的篇幅,讨论下微服务以及这种架构风格的优点和缺点。\n\n在微服务出现之前,我们的架构多数是单体应用架构。会有一个或者少数几个”巨无霸“进程,里面可能包含了”用户管理“、”订单管理“、”"
  },
  {
    "path": "legacy/architecture/toolchain.md",
    "chars": 1065,
    "preview": "# 研发工具链概览\n\n子曰:“工欲善其事,必先利其器”。前面已经提到,微服务架构的对开发水平提出了更高的=要求,我们更应该注重研发工具链的建设,以提高开发效率。\n\n* 内部帐号管理:不论大小企业,无论企业性质,都需要一个集中式的帐号管理系统"
  },
  {
    "path": "legacy/devops/README.md",
    "chars": 759,
    "preview": "# 运维工具链\n\n如果你一直从事研发职位,或很少接触运维岗位,可能会觉得将\"运维\"放到架构设计中,有一些多余。\n\n事实上,上述想法大错特错,我们来看几个案例:\n1. \"刚才的上线,研发人员是从test分支打的jar包,引入了新的bug,赶快"
  },
  {
    "path": "legacy/devops/discovery.md",
    "chars": 20,
    "preview": "# Nginx REST网关自动配置\n\n"
  },
  {
    "path": "legacy/devops/docker-repo.md",
    "chars": 4217,
    "preview": "# Docker 私有仓库\n\n在前面的章节中,我们使用了Kubernetes和容器技术实现了微服务的发现、负载均衡、持续部署等需求。\n\n然而,我们并未提到Docker镜像的配置。默认的,我们使用了Docker官方默认的Docker镜像。\n\n"
  },
  {
    "path": "legacy/devops/jump-server.md",
    "chars": 9,
    "preview": "# 线上跳板机\n\n"
  },
  {
    "path": "legacy/devops/openvpn-k8s.md",
    "chars": 5129,
    "preview": "# OpenVPN访问Kubernetes集群内网\n\n搭建好的Kubernetes集群中,默认是存在网络隔离的,即集群内部使用一套独立的网络,与物理网络相互隔离。\n\n为了将内部服务暴露给外部调用,Kubernetes提供了ClusterIP"
  },
  {
    "path": "legacy/k8s/README.md",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "legacy/k8s/docker-k8s.md",
    "chars": 2157,
    "preview": "# 集装箱、容器化、容器编排\n\n## 集装箱革命、容器化革命\n\n前面已经提到,微服务架构离不开容器技术。为什么需要容器呢?\n\n我们先来看一个海运的例子。\n\n* 传统海运:尽管货海运已经出现了几千年,但直到20世纪中叶,货物运输依然是一种劳动"
  },
  {
    "path": "legacy/k8s/helm.md",
    "chars": 4728,
    "preview": "# 使用Helm进行包管理\n\n通过前面的章节,我们已经学会了如何在Kubernetes上启动Pod、Deployment及Service。\n\n我们再来简单回顾一下流程过程(以Service为例):\n\n编写yaml文件,其中要包含如下信息:\n"
  },
  {
    "path": "legacy/k8s/k8s-cluster.md",
    "chars": 7596,
    "preview": "# 搭建Kubernetes集群\n\nminikube是入门Kubernetes的优秀工具。使用minikube,可以轻松地在本地运行Kuberntes的主要功能。\n\n为了演示方便,本书如有涉及到Kubernetes的章节,多数都是在mini"
  },
  {
    "path": "legacy/k8s/k8s-ha.md",
    "chars": 14153,
    "preview": "# Kubernetes集群的高可用方案\n\n高可用(High Availability)是指系统可以“无中断”地提供服务的能力。\n\nKubernetes作为容器调度和编排的“操作系统”,高可用显得尤为重要。\n\n在之前的章节,我们搭建的都是“"
  },
  {
    "path": "legacy/k8s/k8s-intro.md",
    "chars": 4770,
    "preview": "# Kubernetes 快速入门\n\n## Kubernetes中的基本操作单元\n\n为了适应复杂的业务需求,Kubernetes中内置了不同层级的操作单元:\n* Pod: Pod是Kubernetes的基本操作单元,也是应用运行的载体。如果"
  },
  {
    "path": "legacy/k8s/k8s-ipvs.md",
    "chars": 2561,
    "preview": "# 为Kubernetes开启ipvs\n\n通过前面的章节,我们已经学会了如何部署一个真正的Kubernetes集群。\n\n此外,你可能也听说过,Kubernetes内置了Service、Deployment等机制,原生支持了负载均衡。\n\nKu"
  },
  {
    "path": "legacy/k8s/k8s-office.md",
    "chars": 3557,
    "preview": "# 办公网与Kubernetes集群的打通\n\n通过前面的章节,我们已经学会了Kubernetes集群的搭建、优化、部署应用。\n\n在本节中,我们将讨论另一个常见的场景:打通办公网与Kubernetes集群。\n\n在测试或者开发环境,经常会有这种"
  },
  {
    "path": "legacy/ms-circuit-breaker-and-limit/README.md",
    "chars": 432,
    "preview": "# 微服务熔断与限流\n\n\"熔断\"、\"限流\"这两个词看起来是和性能相关的。你可能会有疑问:微服务架构支持横向拓展,性能不够加机器就可以,为什么还需要熔断呢?\n\n是的,横向拓展确实可以提升性能,但同时也降低了可用性。假设一个服务的可用性是99."
  },
  {
    "path": "legacy/ms-circuit-breaker-and-limit/sb-hystrix.md",
    "chars": 6770,
    "preview": "# 熔断与Hystrix\n\n在本节,我们将讨论\"熔断\"方案的思路及其在微服务架构下的落地。\n\n\"熔断\"这个词来源于电路保护。如果一条线路上的的电压过高,就会将保险丝烧断,从而切断该条线路上的电流,防止其影响其他线路。\n\n我们将上述场景对应到"
  },
  {
    "path": "legacy/ms-circuit-breaker-and-limit/sb-limit.md",
    "chars": 8280,
    "preview": "# 限流的实现\n\n与\"熔断\"类似,\"限流\"也是一种降级手段,但限流的思路更简单、直观: 它直接拒绝部分请求。\n\n在微服务架构下,若大量请求超过微服务的处理能力时,可能会将服务打跨,甚至产生雪崩效应、影响系统的整体稳定性。\n\n孙子兵法有一计\""
  },
  {
    "path": "legacy/ms-config/README.md",
    "chars": 485,
    "preview": "# 微服务配置中心\n\n\"配置中心\"是对配置进行集中管理的系统,是微服务架构中的一个基础环节。\n\n服务端存在多种类型的配置:\n* 环境变量:如操作系统环境变量\n* 内部配量:如阈值、关键字\n* 应用配量:如功能、特性开关\n\n环境变量等配置很少"
  },
  {
    "path": "legacy/ms-config/cfg4j.md",
    "chars": 1337,
    "preview": "# cfg4j及方案简介\n\n实现微服务的配置中心有多种选择方案,常见的方案有:\n* 使用Spring Cloud全家桶中的Spring Cloud Config。\n* 使用Consul或者Zookeeper作为分布式一致性存储,自己实现配置"
  },
  {
    "path": "legacy/ms-config/consul-devops.md",
    "chars": 868,
    "preview": "# Consul服务的运维\n\nConsul是一款支持高可用的服务发现、配置管理服务。我们使用Consul作为配置中心的基础服务。即由Consul提供配置的管理、获取等基础功能。\n\n在探讨配置中心之前,我们首先来看一下Consul的运维工作。"
  },
  {
    "path": "legacy/ms-config/sb-config.md",
    "chars": 8208,
    "preview": "# Spring Boot整合配置中心\n\n上一小节中,我们探讨了如何利用gerrit搭建配置中心的版本仓库。\n\n现在,我们探讨如何在Spring Boot的框架中整合配置中心。\n\n## 开发lmsia-cfg4j库,实现配置项的自动注入\n\n"
  },
  {
    "path": "legacy/ms-delivery/README.md",
    "chars": 536,
    "preview": "# 微服务持续交付\n\n\"持续集成\"(Continuous integration):频繁地将开发代码合并到主干,并保证可以编译通过,并通过基本额单元测试。\n坚持持续集成的优点有:\n* 快速失败(Fast Fail): 尽早发现错误,\"早发现"
  },
  {
    "path": "legacy/ms-delivery/jenkins-devops.md",
    "chars": 3160,
    "preview": "# Jenkins构建平台的运维\n\nJenkins是一款开源的持续构建工具,除了基础功能外,还有各种功能丰富的插件,可以实现各种高级功能。\n\nJenkins常见的应用场景是:\n* 项目的自动构建(编译),即持续集成\n* 自动执行项目的单元/"
  },
  {
    "path": "legacy/ms-delivery/ms-cd.md",
    "chars": 2755,
    "preview": "# Jenkins持续部署\n\n在上一小节,我们完成了Jenkins的持续集成工作,经过持续集成,我们的代码已经编译成Docker镜像,并被Push到私有仓库中。\n\n在本节,我们接着前一小节的成功,讨论部署问题。\n\n这里的部署指的是将微服务真"
  },
  {
    "path": "legacy/ms-delivery/ms-ci.md",
    "chars": 7661,
    "preview": "# Jenkins持续集成\n\n在本小节中,我们将讨论持续集成。\n\n持续集成指的是: 频繁地(一天多次)将代码集成到主干。这里的集成不止是代码合并,还要保证可以通过编译、单元测试、集成测试。\n\n持续集成的主要优点是:\n* 快速发现错误。即所谓"
  },
  {
    "path": "legacy/ms-discovery/README.md",
    "chars": 388,
    "preview": "# 微服务的自动发现与负载均衡 \n\n采用微服务架构后,巨无霸服务被拆分为若干逻辑独立的微服务,导致服务数量逐渐上升。此外,为了保证系统的高可用和高性能,每一个微服务都会运行若干副本,这更进一步地导致微服务运行实例数量的攀升。\n\n面对数量逐渐"
  },
  {
    "path": "legacy/ms-discovery/msd.md",
    "chars": 5936,
    "preview": "# 微服务的自动发现\n\n在熟悉了的基本操作后,我们来讨论下如何实现微服务的自动发现。\n\nService是在Pod基础上做的另一层抽象,通过虚拟IP的方式,提供了统一的代理入口和负载均衡。\n\nService本身不会创建Pod,而是通过标签的方"
  },
  {
    "path": "legacy/ms-discovery/service-discovery.xml",
    "chars": 1417,
    "preview": "<mxfile userAgent=\"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/53"
  },
  {
    "path": "legacy/ms-log/README.md",
    "chars": 561,
    "preview": "# 微服务日志监控\n\n与前端或移动端产品不同,微服务运行于后台,我们不能直观的观察到服务端的运行状况。因此,合理地记录日志是检查服务端运行状态,查找问题的有效手段。\n\n服务端日志的常见用途有:\n* 业务日志:对一些分支条件进行简单记录,方便"
  },
  {
    "path": "legacy/ms-log/elk-devops.md",
    "chars": 5835,
    "preview": "# ELK日志分析平台的运维\n\n在上一节中,我们在日志文件中增加了调用链信息,方便我们追踪每一次调用的完整关系链条。\n\n尽管有了追踪信息,可以更好地排查信息。但在微服务架构下,微服务众多,每个微服务又会启动若干个副本,日志文件的数量会随着文"
  },
  {
    "path": "legacy/ms-log/sb-eblk.md",
    "chars": 4273,
    "preview": "# Spring Boot整合EBLK日志分析平台\n\n不知道你有没有注意到,这一节的标题是\"EBLK日志分析平台\",而上一节的标题中是\"ELK日志分析平台\"。是的,你没有看错,这也不是笔误。\n\n在ELK平台中,Logstash负责收集微服务"
  },
  {
    "path": "legacy/ms-log/sb-logback.md",
    "chars": 4987,
    "preview": "# Spring Boot配置Logback及HTTP日志\n\n系统上先后,需要进行一系列的运维、监控工作,可能还需要排查业务故障和系统问题。\n\n服务已经上线了,不能像本地开发的一样“”打断点调试“,此时,日志的作用就非常重要了。\n\n与其他服"
  },
  {
    "path": "legacy/ms-log/sb-trace.md",
    "chars": 8069,
    "preview": "# Spring Boot整合分布式调用链追踪\n\n在上一节,我们讨论了如何在Spring Boot项目中配置LogBack日志系统。\n\n如果是传统的巨服务架构,有日志就能够满足基本的需求了。\n\n但面对微服务,事情变得有一些复杂:\n* 微服务"
  },
  {
    "path": "legacy/ms-monitor/README.md",
    "chars": 9,
    "preview": "# 微服务监控\n\n"
  },
  {
    "path": "legacy/ms-monitor/k8s-prometheus-grafana.md",
    "chars": 4538,
    "preview": "# Kubernetes + Prometheus + Grafana监控平台\n\n平台监控是微服务架构中的重要一环。\n\n例如,一个很常见的场景,某个微服务突然响应变慢,之前都是100毫米内返回,现在需要2秒才能返回结果,导致大量下游服务超时"
  },
  {
    "path": "legacy/ms-monitor/sb-prometheus.md",
    "chars": 16,
    "preview": "# 整合Prometheus\n\n"
  },
  {
    "path": "legacy/ms-monitor/sb-sentry.md",
    "chars": 3549,
    "preview": "# Spring Boot整合Sentry\n\n在上一小节中,我们探讨了如何运维Senty系统。\n\n搭建好的Sentry系统,需要接入错误事件的数据源,才能发挥功效。\n\n在本节中,我们探讨如何将Spring Boot的微服务项目与Sentry"
  },
  {
    "path": "legacy/ms-monitor/sentry-devops.md",
    "chars": 4610,
    "preview": "# Sentry 错误预警系统的运维\n\n在上一章中,我们介绍了EBLK的日志分析平台。\n\n在日志分析平台上,我们可以很方便的查找系统的日志。然而,EBLK并总是能满足需求:\n* 日志绝大多数是INFO等级的,即信息日志。如果系统运行出现问题"
  },
  {
    "path": "legacy/ms-monitor/sentry.txt",
    "chars": 70,
    "preview": "https://laravel-china.org/articles/4285/build-your-own-sentry-service\n"
  },
  {
    "path": "legacy/ms-msgq/README.md",
    "chars": 12,
    "preview": "# 微服务的消息队列\n\n"
  },
  {
    "path": "legacy/ms-msgq/dev-kafka.md",
    "chars": 4829,
    "preview": "# Kafka 流处理开发简介\n\n在上一节,我们介绍了分布式流处理平台Kafka的运维工作,在这一节,我们将讨论Kafka的应用开发。\n\n你可能已经注意到,这一节的标题并不是\"在微服务中的集成\",而是\"开发简介\"。\n\n使得,如在前文所述,K"
  },
  {
    "path": "legacy/ms-msgq/kafka-devops.md",
    "chars": 5492,
    "preview": "# Kafka流处理平台的运维\n\nKafka是高性能的分布式流处理平台,它的特点有:\n* 类似于传统的消息队列,为海量流式数据提供了消息发布/订阅模型。\n* 支持容错的流式数据存储。\n* 流式数据的实时处理。\n\nKafka是一款吞吐性能非常"
  },
  {
    "path": "legacy/ms-msgq/rabbitmq-devops.md",
    "chars": 5792,
    "preview": "# RabbitMQ 消息队列\n\nRabbitMQ支持单机部署,也提供了\"高可用\"的集群部署方式,以提升性能和(或)可用性。\n\nRabbitMQ支持两种形式的集群模式:\n* 普通模式: 默认的模式。消息队列的数据结构存在于每个节点上,但实际"
  },
  {
    "path": "legacy/ms-msgq/rocketmq-devops.md",
    "chars": 5920,
    "preview": "# RocketMQ 消息队列的运维\n\n在前两节,我们讨论了RabbitMQ。\n\n然而根据实际的生产经验来看,当系统瞬时流量达到一定规模时,上述两款产品都不再适合作为消息系统的首选。\n\nRabbitMQ在企业级应用是没有问题的,但它的抗消息"
  },
  {
    "path": "legacy/ms-msgq/sb-kafka.md",
    "chars": 22,
    "preview": "# Spring Boot整合Kafka\n\n"
  },
  {
    "path": "legacy/ms-msgq/sb-rabitmq.md",
    "chars": 1598,
    "preview": "# Spring Boot整合RabbitMQ\n\n在上一节,我们已经掌握了RabbitMQ集群的运维方法。\n\n在本章中,我们来看一下如何在Spring Boot中集成RabbitMQ\n\n## 依赖配置\nRabbitMQ实现了AMQP协议,因"
  },
  {
    "path": "legacy/ms-msgq/sb-rocketmq.md",
    "chars": 1753,
    "preview": "# Spring Boot整合RocketMQ\n\n在本小节中,我们将讨论在Spring Boot中整合RocketMQ。\n\n我们选用官方推荐的Spring Boot拓展[rocketmq-spring-boot-starter](https"
  },
  {
    "path": "legacy/ms-storage/README.md",
    "chars": 422,
    "preview": "# 微服务的存储与缓存\n\n在计算机领域,有一句人尽皆知的名言\"算法=程序+数据结构\",这是图灵奖获得者尼古拉斯·沃斯提出的[^1]。\n\n从大师的这句名言中,我们不难感受到,数据的存储方式与算法同等重要。\n\n在微服务架构中,我们虽然不会研究特"
  },
  {
    "path": "legacy/ms-storage/memcached-devops.md",
    "chars": 2728,
    "preview": "# Memcached 缓存服务的运维 \n\n如果业务进一步发展,通过\"读写分离\"、\"分库分表\"后,数据库的性能依然无法满足高并发读请求,此时就需要缓存出马了。\n\n缓存的原理其实非常简单: 用\"存取速度更快的空间\"换取\"存取速度更慢的时间\"。"
  },
  {
    "path": "legacy/ms-storage/mysql-devops.md",
    "chars": 8112,
    "preview": "# MySQL数据库的运维\n\n近几年,以\"Redis\"、\"MongoDB\"为代表的\"NoSQL\"数据库迅速崛起。\"NoSQL\"并不是\"没有SQL\"而是\"Not Only SQL\"。在特定场景下,NoSQL数据库确实解决了一些问题,例如:\n\n"
  },
  {
    "path": "legacy/ms-storage/redis-devops.md",
    "chars": 7531,
    "preview": "# Redis 数据库的运维\n\n作为纯内存缓存,Memcached拥有非常出色的读写性能,但也存在一个较为严重的缺点:无法持久化。\n\n这意味着,一旦Memcached服务重启(更常见的是掉电),之前所有的缓存就会丢失。若线上的流量很大,这种"
  },
  {
    "path": "legacy/ms-storage/sb-memcached.md",
    "chars": 14016,
    "preview": "# Spring Boot整合Memcached\n\n前面已经提到,缓存是快速提升系统性能,缓解瓶颈的有效手段。\n\n缓存的种类多种多样,小到CPU的缓存,大到静态生成的页面缓存。在本小节中,我们主要讨论在Spring Boot中整合如下两种缓"
  },
  {
    "path": "legacy/ms-storage/sb-mysql.md",
    "chars": 8200,
    "preview": "# Spring Boot整合MySQL\n\n经过上一节的讨论,相信你已经有了一套可运维的MySQL服务器了,接下来的两节,我们来讨论如何在Spring Boot中整合MySQL。\n\n在Spring Boot中整合MySQL有很多方式,常见的"
  },
  {
    "path": "legacy/ms-storage/sb-redis.md",
    "chars": 7796,
    "preview": "# Spring Boot整合Redis\n\n在上一章中,我们讨论了Redis服务的运维,包括单机运行和Sentinel运行。\n\n在本小节中,我们讨论如何在Spring Boot中集成Redis。\n\nSpring Boot内置了Redis的接"
  },
  {
    "path": "legacy/spring-boot/README.md",
    "chars": 442,
    "preview": "# 微服务的开发框架 \n\n在[上一章](../ms-discovery/README.md),我们解决了微服务的第一个核心问题\"服务发现\"。\n\n本章我们将回归开发本质,开始上手微服务的开发框架。\n\n本书选用Java作为微服务的开发语言,选用"
  },
  {
    "path": "legacy/spring-boot/discovery.md",
    "chars": 26,
    "preview": "# Spring Boot + k8s 服务发现\n\n"
  },
  {
    "path": "legacy/spring-boot/gerrit.md",
    "chars": 27,
    "preview": "# Spring Boot 多模块Gradle项目\n\n"
  },
  {
    "path": "legacy/spring-boot/graceful-shutdown.xml",
    "chars": 1517,
    "preview": "<mxfile userAgent=\"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.170 Safari/53"
  },
  {
    "path": "legacy/spring-boot/mockito.md",
    "chars": 20,
    "preview": "# Mockito 单元测试打桩神器\n\n"
  },
  {
    "path": "legacy/spring-boot/rest-nginx.xml",
    "chars": 1929,
    "preview": "<mxfile userAgent=\"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/53"
  },
  {
    "path": "legacy/spring-boot/sb-gradle-structure.md",
    "chars": 8616,
    "preview": "# Gradle子项目划分与微服务的代码结构\n\n## Gradle简介\n\n如前序章节[微服务技术栈概览](../architecture/microservics.md)所述,本书选用Java作为开发语言、Gradle作为构建工具。\n\n与M"
  },
  {
    "path": "legacy/spring-boot/sb-mockito.md",
    "chars": 4393,
    "preview": "# Mockito 单元测试打桩神器\n\n## 单元测试\n\n软件测试是软件质量保证的关健环节,代表了需求、设计和编码的最终检查。\n\n![软件测试金字塔](./test.jpg \"软件测试金字塔\")\n\n如上图所示,测试金字塔将测试分为三类\n* "
  },
  {
    "path": "legacy/spring-boot/sb-rest.md",
    "chars": 7817,
    "preview": "# Spring Boot REST接口\n\n在介绍服务发现和负载均衡时已经提到,我们的架构中,对每个微服务开放两个虚拟IP端口,一个是RPC,另外一个是REST(HTTP)。\n\n在上一节中,我们探讨了Spring Boot中集成Thrift"
  },
  {
    "path": "legacy/spring-boot/sb-thrift.md",
    "chars": 36745,
    "preview": "# Spring Boot整合Thrift RPC\n\n## Spring Boot自动配置简介\n\n在介绍RPC之前,我们先来学习下Spring Boot的自动配置。\n\n我们前面已经提到:Spring Boot来源于Spring,并且做了众多"
  },
  {
    "path": "legacy/toolchain/README.md",
    "chars": 332,
    "preview": "# 研发工具链\n\n子曰: \"工欲善其事必先利其器\"。\n\n本书的开篇已经指出,微服务的架构对研发人员提出了更高的要求。\n\n幸运的是,通过不断完善、改进研发工具链,可以为研发人员提供更高效、更便捷的开发环境。\n\n本书反复强调\"微服务\"、\"研发工"
  },
  {
    "path": "legacy/toolchain/bom.md",
    "chars": 4128,
    "preview": "# BOM 减少版本冲突\n\n在应用了Gradle构建工具,以及Maven仓库来管理版本依赖后,程序的构建、依赖问题已经得到了基本的解决。\n\n但随着项目的不断发展,一个微服务的依赖可能会越来越多,出现版本冲突的问题。\n\n举个版本冲突的例子:项"
  },
  {
    "path": "legacy/toolchain/gerrit.md",
    "chars": 9422,
    "preview": "# gerrit 代码的版本管理与审查\n\n## 为什么选用git作为版本管理系统\n\n在实际工作中,绝大多数的项目都使用了代码的版本管理系统。在应用版本管理系统后,可以代码许多好处,相信大家有有所体会:\n* 团队合作: 应用版本管理系统后,每"
  },
  {
    "path": "legacy/toolchain/kanboard.md",
    "chars": 20,
    "preview": "# Kanboard Scrum看板\n\n"
  },
  {
    "path": "legacy/toolchain/ldap.md",
    "chars": 9087,
    "preview": "# LDAP 内部账号管理系统\n\n## LDAP及其必要性 \n\n对于任何一个研发团队,一套内部通用的帐号管理系统都是必不可少的。请注意我的用词:\"内部通用\"。\n\n公司内部可能有各种系统:\n* 行政层面的OA系统、邮件系统、会议室预订系统。\n"
  },
  {
    "path": "legacy/toolchain/nexus.md",
    "chars": 7415,
    "preview": "# Nexus 私有maven仓库\n\n依赖管理是技术栈的重要一环,几乎所有的现代编程语言都拥有自己的依赖管理系统。\n\n如果你在很久以前就从事了Java开发,或者参与过一些\"不太正规\"的项目,一定经历有过\"jar包随便拷、jar包满天飞\"的经"
  },
  {
    "path": "legacy/toolchain/spring-boot-scripts.md",
    "chars": 8,
    "preview": "# 懒人脚本\n\n"
  },
  {
    "path": "legacy/toolchain/spring-boot-template.md",
    "chars": 20,
    "preview": "# Spring Boot 项目模板\n\n"
  },
  {
    "path": "legacy/toolchain/stress-test.md",
    "chars": 5625,
    "preview": "# 打压工具\n\n当业务刚刚起步的时候,微服务的稳定性是我们的首要保障目标,即服务能否稳定运行而不会挂掉。\n\n随着业务逐渐发展,用户数据量不断增大,并发的请求数也会不断加大。慢慢地,性能问题也会逐渐暴露出来。\n\n典型的性能问题有:\n1. 服务"
  },
  {
    "path": "src/README.md",
    "chars": 1907,
    "preview": "# 从0到1实战微服务架构(第2版)\n\n## 地址汇总\n\n* [Github项目 求Star:-)](https://github.com/liheyuan/hands-on-microservices)\n* [在线阅读](https://"
  },
  {
    "path": "src/SUMMARY.md",
    "chars": 2010,
    "preview": "# [从0到1实战微服务架构](./README.md)\n\n- [前言](./README.md)\n\n- [微服务概述](./ch01-architecture/micro-service-intro.md)\n  \n  - [微服务研发工具"
  },
  {
    "path": "src/ch01-architecture/README.md",
    "chars": 204,
    "preview": "# 第1章 微服务架构概述\n\n当我们讨论服务端的架构时,“微服务”已经成为了最热门的关键字。\n\n如果没有接触过\"微服务\",那么你的心里一定存在很多号?\n\n不要急,我们将从三个基本问题谈起:\n\n- 什么是“微”服务?\n\n- 为什么需要微服务?"
  },
  {
    "path": "src/ch01-architecture/continuous-x.md",
    "chars": 1506,
    "preview": "# 持续集成、持续部署、持续交付\n\n标题里的三个“持续”在前几年特别火热,属于技术热词(BuzzWord)。\n\n持续交付(Continuous Delivery)由马丁·福勒(Martin Fowler)于2006年提出。\n\n是的,你没看错"
  },
  {
    "path": "src/ch01-architecture/micro-service-intro.md",
    "chars": 3286,
    "preview": "# 微服务概述\n\n## 什么是“微”服务?\n\n如果你仔细观察,会发现我在上一行的标题中,将“微”打了个引号。\n\n如果我们暂时去掉这个''微\"字理解,微服务就是我们熟知的“服务端” 或者 “后端”。\n\n现在让我们把微字加回来:-)\n\n\"微服务"
  },
  {
    "path": "src/ch01-architecture/ms-arch.plantuml",
    "chars": 1737,
    "preview": "@startuml\n\ntitle \"微服务架构实现\"\n\npackage \"聚合接入层\" as l5 {\n    [聚合服务1] as n51\n    [聚合服务2] as n52\n    [聚合服务3] as n53\n    [Paddin"
  },
  {
    "path": "src/ch01-architecture/ms-architecture.md",
    "chars": 2747,
    "preview": "# 一种微服务的分层架构\n\n在上一小节,我们讨论了微服务架构“的的特征、优缺点等话题。\n\n你可能对微服务有了一个模糊的概念,依然感觉不够清晰。\n\n这种感受能够理解。因为,微服务的理论只是提供了一种“架构风格”的建议,并不包含具体的实施方案。"
  },
  {
    "path": "src/ch01-architecture/ms-tech-stack.md",
    "chars": 4027,
    "preview": "# 一种微服务分层架构的技术栈选型\n\n我们在[工具链](./rd-ops-toolchain.md)、[一种微服务的分层架构](./ms-architecture.md) 两小节中讨论了技术栈的需求。\n\n在本节中,我们将具体讨论技术栈的选型"
  },
  {
    "path": "src/ch01-architecture/rd-ops-toolchain.md",
    "chars": 2301,
    "preview": "## 微服务研发工具链\n\n> 子曰:“工欲善其事,必先利其器。居是邦也,事其大夫之贤者,友其士之仁者。” \n> \n>                                                              "
  },
  {
    "path": "src/ch02-ms-dev1/README.md",
    "chars": 650,
    "preview": "# 微服务开发上篇:开发框架及其与RPC、数据库、Redis的集成\n\n从这一章开始,我们正式进入微服务开发篇,共分上、中、下三篇。\n\n本章我们将讨论开发框架,框架与RPC、数据库、Redis的集成。\n\n2001年,我刚开始编程时,接触的第一"
  },
  {
    "path": "src/ch02-ms-dev1/database1.md",
    "chars": 5052,
    "preview": "# Spring Boot集成SQL数据库1\n\n从银行的交易数据到打车订单,衣食住行,都离不开数据库的存储。\n\n在接下来的两个小节中,我们将通过3种不同的技术,在Spring Boot中集成MySQL数据库。\n\n- JDBC\n\n- MyBa"
  },
  {
    "path": "src/ch02-ms-dev1/database2.md",
    "chars": 4778,
    "preview": "# Spring Boot集成SQL数据库2\n\n## Spring Boot 集成 MyBatis操作MySQL\n\nMyBatis是一款半自动的ORM框架。由于某国内大厂的广泛使用,MyBatis在国内非常火热(在国外其热度不如Hibern"
  },
  {
    "path": "src/ch02-ms-dev1/gradle.md",
    "chars": 3139,
    "preview": "# Gradle构建工具配置\n\n构建工具解决了依赖管理、打包流程、项目结构工程化等问题,是现代软件开发中的必备工具。\n\nGradle是一款Java开发语言的构建工具,兼容POM以来,使用Groovy作为描述语言,构建速度快、可拓展性强,是大"
  },
  {
    "path": "src/ch02-ms-dev1/redis.md",
    "chars": 6923,
    "preview": "# Spring Boot集成Redis内存数据库\n\n常规的业务数据,一般选择存储在SQL数据库中。\n\n传统的SQL数据库基于磁盘存储,可以正常的流量需求。然而,在高并发应用场景中容易被拖垮,导致系统崩溃。\n\n针对这种情况,我们可以通过增加"
  },
  {
    "path": "src/ch02-ms-dev1/rpc.md",
    "chars": 8908,
    "preview": "# Spring Boot集成gRPC框架\n\ngRPC是谷歌开源的高性能、开源、通用RPC框架。由于gRPC基于HTTP2协议,所以其对移动端非常友好。\n\n本节将介绍Spring Boot集成gRPC的服务端、客户端。\n\n### 安装pro"
  },
  {
    "path": "src/ch02-ms-dev1/spring-boot.md",
    "chars": 8966,
    "preview": "# Sprint Boot项目与Gradle的集成\n\n本节我们将借助Spring Start快速搭建微服务项目。\n\n在此基础上,我们会将工程改造成子项目的组织形式。\n\n## Spring Start快速生成项目\n\n为了降低微服务的开发门槛,"
  },
  {
    "path": "src/ch03-ms-dev2/README.md",
    "chars": 474,
    "preview": "# 微服务开发中篇:微服务的注册与发现、配置中心、消息队列、稳定性\n\n你可能留意到,在\"微服务上篇\"的讨论中,我们介绍的RPC、数据库等内容,都局限于单机环境,并没有真正涉及“分布式”。\n\n在本章,我们将\"真正的\"进入分布式的微服务实战开发"
  },
  {
    "path": "src/ch03-ms-dev2/circuit-breaker-and-limiter.md",
    "chars": 26770,
    "preview": "# Spring Boot集成熔断、限流、降级\n\n在引入resilience4j之前,我们先来讨论下服务稳定性的三大法宝。\n\n- 降级:在有限资源情况下,为了应对超负荷流量,适当放弃一些功能,以保证服务的整体稳定性。例如:双十一大促时,关闭"
  },
  {
    "path": "src/ch03-ms-dev2/config.md",
    "chars": 10390,
    "preview": "# Spring Boot集成配置中心\n\nNacos不仅提供了服务的注册与发现,也提供了配置管理的功能。\n\n本节,我们继续使用Nacos,基于其配置管理的功能,实现微服务的配置中心。\n\n首先,我们在Nacos上,新建两个配置:\n\n![f]("
  },
  {
    "path": "src/ch03-ms-dev2/mq.md",
    "chars": 11558,
    "preview": "## Spring Boot集成消息队列\n\n[Apache RocketMQ](https://rocketmq.apache.org/)是由开源的轻量级消息队列,于2017年正式成为Apache顶级项目。\n\n在分布式消息队列中间件领域,最"
  },
  {
    "path": "src/ch03-ms-dev2/registry1.md",
    "chars": 3483,
    "preview": "# Nacos注册中心:注册篇\n\n![f](amazon-ms-structure.png)\n\n这是一张从互联网上找到的图,你的直观感受是什么?头皮发麻?\n\n实际上,这个球儿是某一年亚马逊的微服务结构图,每一个球的端点,都是一个微服务。\n\n"
  },
  {
    "path": "src/ch03-ms-dev2/registry2.md",
    "chars": 8829,
    "preview": "# Nacos注册中心:发现篇\n\n经过上一节的努力,我们已经将RPC服务成功的注册到Nacos上了。\n\n我们还是以老生常谈的A调用B为例,B的所有实例B1、B2...都在Nacos上了。我们本节要实现的,都客户端,也就是A的部分。\n\n老规矩"
  },
  {
    "path": "src/ch04-ms-dev3/README.md",
    "chars": 460,
    "preview": "## 微服务开发下篇:日志、链路追踪、监控\n\n随着微服务架构的流行,可观测性(Observability)的理念也逐渐升温。\n\n可观测性是一个源于控制论的概念,映射到微服务架构中,主要指三个方面:\n\n- 日志:微服务的进程产生日志,分散在各"
  },
  {
    "path": "src/ch04-ms-dev3/elkfk.md",
    "chars": 17119,
    "preview": "# 基于ELKFK打造日志平台\n\n微服务的实例数众多,需要一个强大的日志日志平台,它应具有以下功能:\n\n- 采集:从服务端进程(k8s的Pod中),自动收集日志\n\n- 存储:将日志按照时间序列,存储在持久化的介质上,以供未来查找。\n\n- 检"
  },
  {
    "path": "src/ch04-ms-dev3/micrometer.md",
    "chars": 8232,
    "preview": "# 基于MicroMeter实现应用监控指标\n\n提到“监控”(Moniter),你的第一反应是什么?\n\n是老传统监控软件Zabbix、Nagios?还是近几年火爆IT圈的Promethos?\n\n别急着比较系统,这篇文章,我们先聊聊应用监控指"
  },
  {
    "path": "src/ch04-ms-dev3/skywalking.md",
    "chars": 4641,
    "preview": "# 基于SkyWalking的链路追踪系统\n\n链路追踪提供了分布式调用链路的还原、统计、分析等功能,是提升微服务诊断效率的重要环节。\n\n本节,我们将基于[SkyWalking](https://skywalking.apache.org/)"
  },
  {
    "path": "src/ch04-ms-dev3/victorialmetrics.md",
    "chars": 8552,
    "preview": "# 基于VictoriaMetrics + Grafana的监控系统\n\n监控(Monitor)与度量(Metrics)是可观测性的重要环节。\n\n在本节中,我们将使用VirtorialMetrics构建自己的监控系统。\n\n提到监控系统的工具,"
  },
  {
    "path": "src/ch05-k8s/README.md",
    "chars": 306,
    "preview": "# 容器与编排系统\n\n最热门的容器方案 - Docker - 诞生于2013年。借助Namespace、cgroup、rootfs三大核心技术,Docker给软件开发、运维都来了颠覆性的体验。\n\n随着容器技术的普及,容器依赖、跨主机通信的需"
  },
  {
    "path": "src/ch05-k8s/container.md",
    "chars": 1679,
    "preview": "# 从集装箱到容器\n\n容器化是一种全新的交付方式,它把应用及运行环境,整体打包成一个的镜像,从而保证了运行环境的统一。\n\n容器也是一种轻量级的隔离技术,在保证文件系统、网络、CPU等基础隔离的基础上,拥有更快的启动速度,更小的资源开销。\n\n"
  },
  {
    "path": "src/ch05-k8s/k8s-101.md",
    "chars": 4381,
    "preview": "# 快速入门Kubernetes\n\n## 安装篇\n\n我们以Ubuntu为例,介绍Kubernetes基础工具的安装,若你使用其他操作系统,可以参考[官方文档](https://kubernetes.io/docs/tasks/tools/)"
  },
  {
    "path": "src/ch05-k8s/k8s-cluster.md",
    "chars": 6449,
    "preview": "# 搭建Kubernetes集群\n\n在本章的前几节,我们在minikube集群上,实战了很多内容,是时候搭建真正的集群了。\n\n本节,我们将借助kubeadm的帮助,搭建准生产级的k8s集群。\n\n关于\"准生产\"的含义,我们先放下不表。\n\n以下"
  },
  {
    "path": "src/ch05-k8s/k8s-ha-cluster.md",
    "chars": 6818,
    "preview": "# 搭建Kubernetes高可用集群\n\n在上一节,我们介绍了Kubernetes集群的搭建,我们说这是一个“准生产”级别的集群。\n\n原因是,他不支持高可用。\n\n设想下,假设Master节点挂掉,会出现什么情况?\n\n由于只有一个主节点,所以"
  },
  {
    "path": "src/ch05-k8s/k8s-ingress.md",
    "chars": 9764,
    "preview": "# 通过ingress暴露内部服务\n\n在kubernetes集群中,有一个常见的需求:如何将内部服务暴露出来,供外部访问?\n\n在[快速入门Kubernetes](k8s-101.md)一节中,我们使用了Service(Load Balanc"
  },
  {
    "path": "src/ch06-cd/README.md",
    "chars": 239,
    "preview": "# 持续交付流水线\n\n持续交付是敏捷开发的一种最佳实践,代码发生变更后,可以自动进行持续集成,测试,并部署到线上系统中。\n\n持续交付贯穿了软件的开发、测试、发布等全生命周期,也是微服务架构的基石。\n\n本节将借助Jenkins + 容器技术,"
  },
  {
    "path": "src/ch06-cd/jenkins-custom.md",
    "chars": 5110,
    "preview": "# Jenkins定制Agent\n\n上一节,我们实现了最简单的打包任务,在这一节,我们将定制所需的打包环境,为CD流水线做准备。\n\n## 手动连接Agent\n\n在上一节,我们使用了Kubernetes集群启动新的Slave节点,你可以沿着这"
  },
  {
    "path": "src/ch06-cd/jenkins-k8s-optimize.md",
    "chars": 14597,
    "preview": "# Jenkins优化Kubernetes部署流水线\n\n在上一节,我们实现了全链路的部署流水线。\n\n本节,我们将继续完善、优化部署水线。\n\n## Gradle加速\n\n首先,在之前的定制Agent中,我们使用了Gradle(Maven)的默认"
  },
  {
    "path": "src/ch06-cd/jenkins-k8s.md",
    "chars": 7979,
    "preview": "# Jenkins实现Kubernetes部署流水线\n\n在Agent定制环境准备好后,我们将构建完整的部署流水线。\n\n根据我们选用的技术栈,部署流水线划分为如下阶段:\n\n1. checkout代码\n\n2. gradle编译\n\n3. 构建Do"
  },
  {
    "path": "src/ch06-cd/jenkins.md",
    "chars": 6733,
    "preview": "## Jenkins搭建入门\n\nJenkins是一款开源、强大的持续集成工具,其前身是Hudson(商用软件)。\n\n本节将介绍Jenkins的搭建。从架构上理解,Jenklins由两类角色组成:\n\n- Controller:主控节点,负责管"
  },
  {
    "path": "src/ch07-tools/.md",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/ch07-tools/README.md",
    "chars": 195,
    "preview": "# 工具链\n\n微服务架构的成功落地,离不开工具链的辅助。\n\n本节将讨论与研发密切相关的工具链,包括\n\n1. 快速生成微服务的模板工具\n\n2. Ldap及内网认证系统\n\n3. 基于Gitlab的私有代码平台\n\n4. 基于JFrog Artif"
  },
  {
    "path": "src/ch07-tools/gitlab.md",
    "chars": 3078,
    "preview": "# 基于Gitlab搭建版本控制平台\n\n做为程序员,你一定使用过GitHub / Gitee等开源代码仓库。\n\n对于公司而言,直接将代码上传到开源仓库,对所有用户公开,会面临诸多问题:\n\n- 泄露商业机密\n\n- 安全漏洞泄露\n\n- 被抄袭、"
  },
  {
    "path": "src/ch07-tools/jfrog-artifactory.md",
    "chars": 5907,
    "preview": "# JFrog Artifactory搭建Maven私有仓库\n\n在本书技术架构中,我们选用了Gradle做为Java的依赖管理工具。\n\n实际上,Gradle只提供了构建的前端,实际使用的还是Maven仓库。\n\n出于安全性、速度等因素的考量,"
  },
  {
    "path": "src/ch07-tools/ldap.md",
    "chars": 3472,
    "preview": "# 基于LDAP的内网统一认证\n\n对于任何公司而言,一套“内部通用”的统一认证系统是必不可少的。\n\n请注意两个关键字:内部、通用。\n\n- 内部:认证系统只在公司内部关联的系统使用,并且需要关联具体的员工信息,如:工号、用户名、邮箱等。\n\n-"
  },
  {
    "path": "src/ch07-tools/microservice-template.md",
    "chars": 7856,
    "preview": "# 微服务模板工具\n\n在微服务架构下,我们经常需要按业务领域进行拆分,新建微服务。\n\n频繁的创建新服务,十分繁琐,本文介绍一种微服务创建的模板工具。\n\n在Maven架构下,我们可以用[ArchType]([Maven &#x2013; Gu"
  },
  {
    "path": "src/ch07-tools/registry2.md",
    "chars": 2312,
    "preview": "## 使用Registry2搭建Docker私有仓库\n\n在[打造持续交付流水线](../ch06-cd/README.md)一章中,在部署前,需要先打包Docker镜像,并上传到DockerHub镜像仓库。\n\nDockerHub是由Dock"
  },
  {
    "path": "src/ch07-tools/seafile.md",
    "chars": 2529,
    "preview": "# 搭建Seafile共享云盘\n\n在企业内部,文件的共享,交换是十分重要的需求。\n\n本节,我们将搭建基于Seafile的共享云盘。\n\n## 安装\n\n首先,确保你的机器上已经安装了docker-compose。\n\n接着,下载最新docker-"
  }
]

About this extraction

This page contains the full source code of the liheyuan/hands-on-microservices GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 128 files (556.3 KB), approximately 211.4k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!