Astrisk Blog

容器化DEVOPS-弹性CI平台系列前言

• kubernetes-docker

最近跟朋友聊这两年自己利用kubernetes-docker在企业做的CI/CD平台,朋友不甚了解,建议写成blog,而且自己也想把这一整套系统/技术整理出来,做些总结。这些blog都会以问题-方案的思路来做,希望这样更能够让大家理解为什么要这样做,以及如果实现。

问题

一般企业的技术栈都比较多,比如常见有PHP,Java,node,Java android,GOlang等等。而我们在搭建内部CI的时候,大部分都是用master-多slave的分布式架构,同时为了提高资源利用率,一台slave都会部署多种技术栈的构建/依赖环境。这样就需要在新的服务器上安装所有的依赖环境,越来越多复杂的环境会导致我们的CI环境维护起来比较麻烦,比如会造成相互之间的干扰,升级一个把另一个给搞坏了。

解决方案

CI还是选择开源的Jenkins来做。但是换种方式,就是把整个CI的各个部分,master和各种slave做成一个个独立的docker images,比如Golang有自己的slave docker images ,node有自己的slave images。这样每种技术栈单独维护自己的构建环境docker images,构建的slave如果升级只需要自己的docker images。然后把整个的CI系统放到kubernetes中,让kubernetes来做容器的调度,当没有构建jobs触发时,只运行一个Jenkins master。当某个环境的jobs被触发,kubernetes会自动拉取对应环境的docker images,并启动container,并执行job,而完成job后kubernetes销毁container,回收资源。

实现

下面先把这套docker化的CI系统实现步骤列出来,然后按照步骤一步步搭建起来。

  1. 搭建kubernetes集群(不在这里介绍)
  2. 部署内网docker register
  3. 部署kubernetes共享存储nfs服务器
  4. 构建Jenkins master base images
  5. 把Jenkins master部署到kubernetes,并安装相关插件
  6. 构建Jenkins slave snlp base images
  7. 在第5步的基础上,构建各个技术栈的base images。比如node snlp images
  8. 在Jerkins master UI中配置kubernetes和相应snlp
  9. 在Jerkins master UI中新建测试job,测试

Tsung Cluster install

• performance

OS Check

Docker Private Registry Harbor Install And Https Configure

• kubernetes-docker

安装环境

#  cat /etc/redhat-release
CentOS Linux release 7.2.1511 (Core)
Python 2.7.5 (default, Aug  4 2017, 00:39:18) #系统自带
# docker -v
Docker version 17.06.1-ce, build 874a737
# docker-compose -v
docker-compose version 1.17.0, build ac53b73

安装docker

https://docs.docker.com/engine/installation/

安装docker-compose

https://docs.docker.com/compose/install/

安装Harbor

Kubernetes Packages Manager Tools - Helm install

• kubernetes-docker

在k8s master服务器进行以下操作,完整后,在master会安装helm的客户端,在k8s中安装server:tiller

kubernetes 存储 NFS 部署

• kubernetes-docker

部署和配置NFS服务器

# vim /etc/exports

 /var/nfs/share *(rw,insecure,sync,no_subtree_check,no_root_squash)

启动NFS相关服务

# systemctl enable rpcbind
# systemctl start rpcbind
# systemctl enable nfs-server
# systemctl start nfs-server
# exportfs

# cd /var/nfs/share
# touch nfs-test #创建测试文件

在kubernetes node 安装 nfs client

# yum install nfs-utils
# mount -F nfs {nfs-server}:/var/nfs/share /tmp
# ls -la /tmp #可以看到测试文件nfs-test
# umount /tmp #卸载

# exportfs -r #更新配置后的刷新命令

k8s yaml中使用NFS

  volumes:
    - name: gradlehome
      nfs:
        server: nfs-server-ip
        path: "/var/nfs/share"  

备注:也可以在k8s中创建pv和pvc来管理持久存储,后面介绍kubernetes-nfs动态存储

Install Docker CE in CentOS 7

• DOCKER

Uninstall old docker

$ sudo yum remove docker \
                  docker-common \
                  docker-selinux \
                  docker-engine

Install using the repository

$ sudo yum install -y yum-utils \
  device-mapper-persistent-data \
  lvm2

$ sudo yum-config-manager \
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo

Linux内核设计和实现-避免死锁的简单规则

• LINUXKERNEL

Basic

《Linux内核设计和实现》书中提到了避免死锁的一些简单规则,具体如下:

RABBITMQ-消息生命周期

• RABBITMQ

转自:http://jzhihui.iteye.com/blog/1567232

(注:分析代码基于RabbitMQ 2.8.2) 当客户端通过basic.publish命令(AMQP定义)发布一个消息时,rabbit需要经过以下几个步骤处理消息:

  1. 根据客户端传来的消息内容及相关属性(目标exchange,routing keys,mandatory及immediate属性)构造一个消息实体;
  2. 根据要投递的exchange及routing keys匹配消息的目标投递队列名称;
  3. 根据队列名称找到对应的处理进程ID;
  4. 通过deliver消息(Erlang消息)向目标进程投递消息实体;
  5. 队列对应进程收到投递的消息后,会试图向关联到此队列的消费者投递消息,如果投递失败,或者直接丢弃,或者将消息入队列,入队列时会根据消息及队列的属性(是否durable)判断是否需要写入磁盘,;
  6. 保存到队列的消息,在下次有消费者关联到对应队列时,试图重新投递,直到投递成功,或者因为过期被丢弃掉。

本文的目的主要是分析上面各个步骤中的主要逻辑。

创建消息实体

消息实体由结构#basic_message表示,包含exchange_name,content,id,is_persistent,routing_keys几个域组成。没什么复杂逻辑,主要就是从客户端拿到这些信息后,再组织成一个#basic_message结构。 在实际投递消息之前,其实还有一层封装:#delivery结构,它只是在#basic_message的基础上封装了几个根据投递行为相关的属性,包含:mandatory(此值影响消息在未能成功路由到一个队列时的应对策略,如果为true,则会向客户端返回一个“无法路由(unroutable)”的错误消息;如果为false,则会直接丢弃消息),immediate(此值影响消息无法立即投递到消费者时的应对策略,如果为true,则会向客户端返回一个“无法投递(undeliverable)”的错误消息;如果为false,则服务器会将消息入队列,但是不保证消息最终会被消费者消费),sender(处理此消息的channel进程),message(对应上面的basic_message),msg_seq_no(从1开始的序列,每收到一个消息加1,此值只有存在AMQP中定义的事务的时候才有效)。

匹配目标队列

主要完成由客户端指定的routing keys匹配到目标队列的功能。在创建消费者时,消费者会指定在目标exchange上的绑定关系(bindings,通过queue.bind命令创建),匹配主要就基于这种绑定关系。在AMQP中,有四种最基本的exchange:direct,fanout,topic,headers。在匹配目标队列时,分成三类:direct和fanout的匹配类似,topic与headers各为一类。

direct与fanout匹配算法

主要代码如下:

match_routing_key(SrcName, [RoutingKey]) ->     find_routes(#route{binding = #binding{source      = SrcName,                                           destination = ’$1’,                                           key         = RoutingKey,                                                      = ’’}},                 []); match_routing_key(SrcName, [|] = RoutingKeys) ->     find_routes(#route{binding = #binding{source      = SrcName,                                           destination = ’$1’,                                           key         = ’$2’,                                                      = ’’}},                 [list_to_tuple([‘orelse’ | [{‘=:=’, ’$2’, RKey} ||                                                RKey <- RoutingKeys]])]).

find_routes(MatchHead, Conditions) ->     ets:select(rabbit_route, [{MatchHead, Conditions, [‘$1’]}]).

(参见[$RABBIT_SRC/src/rabbit_router.erl –> match_routing_key/2]) SrcName为目标exchange的名称;direct的exchange在匹配时,传入的第二个参数是客户端发送的routing keys,fanout传入的是[‘’]。 从上面代码可以看出,匹配主要使用ets:select/2函数来完成。只有一个routing key时,match_routing_key函数完成的功能很简单:按照exchange名称(source),routing key(key)在rabbit_route表中进行匹配,并把匹配记录的队列名称(destination)返回。其中rabbit_route表是在客户端通过queue.bind命令(AMQP定义)绑定队列与exchange时,由rabbit写入的,参见[$RABBIT_SRC/src/rabbit_binding.erl –> add/2]。 fanout类型的exchange在匹配时传入[‘’],会匹配到关联到exchange的所有队列。

当有多个routing keys时,find_routing中最终传入的第二个参数会类似如下形式: [‘orelse’, {‘=:=’, ‘$2’, RKey1}, {‘=:=’, ‘$2’, RKey2}, {‘=:=’, ‘$2’, RKey1}, {‘=:=’, ‘$2’, RKey3}] 什么意思呢,{‘=:=’, ‘$2’, RKey1}是说 RKey1与$2所代表的变量要精确相等,第一个元素’orelse’代表其它的元素是“或”关系,也就是说只要#route结构中的key匹配RKey1, RKey2, RKey3中的任意一个就会返回匹配的队列名称。

topic匹配算法

topic的exchange基于类似正则表达式的方式来说明routing pattern(同bindings)。AMQP对用于topic类型exchange的routing key(由生产者在发布消息时指定)有如下规定:由0个或多个以”.”分隔的单词;每个单词只能以一字母或者数字组成。routing pattern增加如下规则:可以匹配单个单词;#可以匹配0个或者多个单词。例如:routing pattern “.stock.#”会匹配到routing key “usd.stock”和“eru.stock.db”,但不会匹配“stock.nasdaq”。

rabbit在收到一个topic类型exchange的绑定请求时,会根据routing pattern生成一个Trie结构:其中的边为以“.”分隔的单词,结点唯一编号。一般的Trie结构会是个树形结构,但是在AMQP的场景下,退化成类似一个链表。对于“*.abc.xyz.#.end”会生成如下结构: base1

(代码参见[$RABBIT_SRC/src/rabbit_exchange_type_topic.erl –> internal_add_binding/1]) 在收到一个消息时,rabbit会根据绑定时创建的trie结构进行搜索,比如对于routing key为test.abc.xyz.123.456.end搜索路径如下: base1

从node1到node2的路径,rabbit会首先尝试用“test”匹配,发现没到直达路径,然后尝试以“”匹配,成功,node2到node3以“abc”匹配成功,node3到node4同理,node4到node5以“#”号匹配,然后在node5结点要跳过任何不能匹配node5到node6路径的单词,这里是“456”,最后匹配到“end”。 rabbit在这个算法的实现上有点奇怪,例如从node2到node3的匹配,即使“abc”路径已经匹配,但还是会尝试通过“”和“#”匹配,增加了很多无意义的比较。 (代码参见[$RABBIT_SRC/src/rabbit_exchange_type_topic.erl –> trie_match/2])

headers匹配算法

AMQP协议对headers类型的exchange的匹配算法有如下规定:由“x-match”头来控制匹配模式,分两种,all和any,类似于布尔运算里的AND和OR:如果匹配模式是all,则目标消息中的头信息的值必须匹配所有在绑定时指定的头信息;如果为any,则只要有一个头信息的值匹配就可以。其中的匹配是指,要么绑定时指定的头信息值为空,要么目标消息中的头信息的值与绑定时指定的值完全一致。 rabbit在实现时,会对绑定时指定的头信息和目标消息中的头信息进行排序(以头信息的键升序排列),然后逐个比对。 想不通这里为什么进行字母序排序(代码里也称这里的匹配算法是Horrendous matching algorithm,具体参见[$RABBIT_SRC/src/rabbit_exchange_type_headers.erl –> headers_match/2]),基于hash map类的数据结构更高效一些。

查找队列进程

每一个队列在创建时,会在rabbit_queue数据表中写入#amqqueue,其中跟进程相关的两个域是pid和slave_pids(在HA策略下slave_pids才有效,代表各个slave结点上的镜像队列的进程ID),pid代表的进程由[$RABBIT_SRC/src/rabbit_amqqueue_process.erl]。 查找的过程其实很简单,就是从rabbit_queue数据表中,根据队列名称找到相应的#amqqueue记录,并将相应的pid和slave_pids全部返回。

投递消息

找到队列对应的处理进程后,通过代理的方式(见[$RABBIT_SRC/src/delegate.erl])向各个队列进程(包含镜像进程)发送deliver消息。rabbit_amqqueue_process在收到deliver消息后,会尝试将消息投递到某个消费者(参见[$RABBIT_SRC/src/rabbit_amqqueue_process –> attempt_delivery/3])。最终会通过[$RABBIT_SRC/src/rabbit_writer.erl –> send_command_and_notify/5]调用以basic.deliver命令(AMQP)将消息内容发送给消费者。如果投递失败,有两种可能的结果:一种直接丢弃,另一种会将消息保存在队列中,等待后续有新的消费者加入时重新投递。保存在队列中的消息会根据durable属性来判断是不是需要写入磁盘,一般情况下,此值为false,消息只保存在内存中,如果需要持久化,此值为true,消息会写入磁盘。

队列机制

跟这部分相关的涉及到rabbit_msg_store模块和rabbit_queue_index模块。而且backing queue为rabbit_mirror_queue_master的队列还涉及到GM(Guaranteed Multicast)相关的东西,所以打算专门写一篇分析这一块。

RABBITMQ-基础知识

• RABBITMQ

RabbitMQ系列blog为《RabbitMQ in Action》的读书笔记,记录相关知识摘要。同时也有些关于RabbitMQ集群以及相关应用的测试方法。

队列

  1. 队列概念
    • 为消息提供了住所,消息在此等待消费
    • 可以起到负载均衡的作用
    • 消息的最后终点(除非消息进入了“黑洞”)
  2. 创建队列
    • 生产者和消费者都可以通过Queue.declare创建队列;如果消费者在同一channel上订阅了另一个队列,则无法再声明队列,必须首先取消队列,将channle置为“传输”模式
  3. 队列设置相关参数
    • Exclusive –true时,队列为私有队列
    • Auto-delete–当最后一个消费者取消订阅时,队列就会自动移除。
  4. 检测队列
    • Queue.declare passive-true,如果队列存在,则命令会成功返回,如果不存在,不会创建队列,会返回一个错误。
  5. 谁创建队列
    • 生产者和消费者都可以创建队列。如果消息被路由到一个不存在的队列,RabbitMQ会忽视消息,消息会丢失。
  6. 订阅队列消息的两种方式
    • Basic.consume - 接收模式
    • Basic.get - 获取单条消息
  7. 消息分发方式
    • 以循环方式发送给多个消息订阅者
  8. 消息确认
    • 消费者通过basic.ack向rabbitMQ发送消息确认,如果订阅到队列时,auto_ack = true,一旦消费者接收到消息,会自动视为确认消息。
    • 消费者确认后,rabbitMQ才能安全地从队列中删除消息
    • 如果消费者没有确认,在确认消息之前,rabbitMQ不会再给该消费者再发送消息
  9. 拒绝消息
    • 把消费者从rabbitMQ服务器断开,消息会重新入队列,并发送给另一个消费者
    • rabbitMQ2.0或以上版本,basic.reject = true,消息会重新发给下一个订阅者;basic.reject=false消息会被从队列中移除,不会发送给新的消费者

交换器和绑定

队列通过规则绑定(routing key)交换器,当消息发送给交换器时,把消息的routing key和交换器绑定的routing key进行进行匹配,如果匹配成功,消息就会进入对应的队列;如果匹配失败,消息进入“黑洞”。

交换器类型

  1. Direct
    • 如果routing key匹配成功,消息就会被投递到对应的队列。服务器必须实现direct类型的交换器,包含一个空白字符串名称的默认交换器。当声明一个队列时,它会自动绑定到默认交换器,并以队列名称做为routing key。
  2. Fanout
    • 该交换器收到消息后,会被广播到绑定到fanout交换器的队列中。属于1 VS N的模式。
  3. Topic
    • 该交换器,可以使得来自不同源头的消息能够到达同一队列。通过在队列绑定时,通过不同位置和类型的通配符来实现。

      Topic exchange can’t have an arbitrary routing_key, it must be a list of words,delimited by dots. Routing key example:”stock.usd.nyse”, “nyse.vmw”, “quick.orange.rabbit”. There can bu as many words in the routing key as you like,up to the limit of 255bytes.

      Two cases for binding keys: *(star) can substitute for exactly one word. #(hash) can sbustitute for zero or more words.

      If we break our contract and send a message with one or four words,like “orange” or “quick.orange.male.rabbit”, these messages won’t match any bindings and will be be lost.

      On the other hand “lazy.orange.male.rabbit”, even though it has four words, will match the queue “lazy.#” and will be delivered to the second queue.

      把msg-inbox模块的error , info , warnning logs投递到msg-inbox-log 队列 $channel -> queue_bind(‘msg-inbox-log’, ‘logs-exchange’, ‘*.msg-inbox’)

      All-log队列将会接收所有从web应用程序发布的所有日志 $channel -> queue_bind(‘all-log’, ‘logs-exchange’, ‘#’)

  4. Header
    • 使用消息的header而非routing key进行消息路由,但是性能比较差,几乎用不到。

多租户模式

Vhost: 每个vhost就像一个mini版的rabbitMQ,拥有自己的队列,交换器和绑定以及权限机制。默认vhost: “/” default user : guest passwd:guest

  1. Vhost 创建:rabbitctl add_vhost [vhost_name]
  2. Vhost 删除:rabbitctl delete_vhost [vhost_name]
  3. Vhost 查看:rabbitctl list_vhosts

消息持久化

队列和交换器的durable属性。该属性默认为false,它决定了rabbitMQ是否需要在崩溃或重启之后重新创建队列(交换器)。

  1. 持久化消息 能从服务器崩溃中恢复的消息。在消息发布前,通过将消息的“投递模式”(delivery mode) 选项设置为2,同时消息还必须被发送到持久化的交换器中。 § 消息投递模式选项设置为2 § 发送到持久化的交换器 § 到达持久化的队列 消息 –> 持久化交换器 –>持久化日志文件 –>持久化队列

    RabbitMQ内建集群环境中,持久化消息工作得并不好。如果集群节点crash,节点上的队列从集群消失,并且持久化队列无法重建,消息也丢失。

  2. 事务

    发送方确认模式–在rabbitMQ2.3.1或更高版本上可用channel上发布的消息都有一个唯一ID,一旦消息到达所有匹配的队列后,channel会发送一个发送方确认模式给生产者app(包含消息的唯一ID)

Linux内核设计和实现-进程状态

• LINUXKERNEL

Linux进程状态

在Linux中每个进程都必然处于以下五种状态中的一种。进程状态描述如下:

进程状态机

process-states