Go 定时器

Go有一个package名字叫time,通过这个package可以很容易的实现与时间有关的操作。time package中有一个ticker结构,可以实现定时任务。

func main(){
    var ticker *time.Ticker = time.NewTicker(1 * time.Second)

    go func() {
        for t := range ticker.C {
            fmt.Println("Tick at", t)
        }
    }()

    time.Sleep(time.Second * 5)  
    ticker.Stop()     
    fmt.Println("Ticker stopped")
}

上面的打印方法会每隔一分钟把当前时间打印出来。修改间隔时间和要执行的函数,就能实现定时任务。


Go 初学者的一些琐碎问题

写了一个 Go 语言的 hello world 来练手。可以在 github 上查看 https://github.com/kelvinblood/ping-pong-go

这个 hello world 主要是 server-client 形式的应用。 功能很简单, client 给 server 发送 ping/pong 的消息,server 回复 pong/ping 消息。

这篇文章记录一些写这个hello world 时遇到的小问题,初学者可以看一看。文末附上源代码。

单双引号

字符串由双引号包裹。单引号内只能有一个字符,例如’c’,输出会返回这个字符的ascii码

字符串相同却不相等

一开始特别奇怪,明明两个变量一摸一样,字符串判断时候却认为他们不一样。。。颇有真假孙悟空的感觉。

接下来就是寻找解决办法的过程

  • 是不是类型不一样:

    • 使用 reflect 包的 reflect.TypeOf 方法,发现都是 string 类型
    • switch 下用Go的空接口: // 建一个函数t 设置参数i 的类型为空接口,空接口可以接受任何数据类型 func t(i interface{}) { //函数t 有一个参数i switch i.(type) { //多选语句switch case string: //是字符时做的事情 case int: //是整数时做的事情 } return }

        // _i.(type)_ 只能在switch中使用
      
  • 是不是像 c 一样数组还有 \n 的诡异操作:

    查看str的长度:

    终于发现了问题所在。

所以解决的办法是只将前面一部分拿来比较:

if strings.EqualFold(recieve[0:len(recieveDefault)],recieveDefault) {
	go send(conn,sendDefault)
  	}else if strings.EqualFold(recieve[0:len(sendDefault)],sendDefault) {
	go send(conn,recieveDefault)
  	}

这里有一篇对比 golang 的 string和[]byte 的区别,写的很好:golang string和[]byte的对比

既然string就是一系列字节,而[]byte也可以表达一系列字节,那么实际运用中应当如何取舍?

  • string可以直接比较,而[]byte不可以,所以[]byte不可以当map的key值。
  • 因为无法修改string中的某个字符,需要粒度小到操作一个字符时,用[]byte。
  • string值不可为nil,所以如果你想要通过返回nil表达额外的含义,就用[]byte。
  • []byte切片这么灵活,想要用切片的特性就用[]byte。
  • 需要大量字符串处理的时候用[]byte,性能好很多。

用到的包

可以直接查看官方文档: https://golang.org/pkg/

  • “fmt”
  • “net”
  • “strings”
  • “time”
  • “math/rand”
  • “reflect”

一些参考资料:

Go语言fmt包Printf方法详解

附源代码

package main

import (
    "fmt"
    "net"
    "strings"
    "time"
    "math/rand"
//    "reflect"
)

func main(){
	_, isClient,err := clientPre()
	
        if err != nil {
		server()
		return
	}

	if isClient {
		client()
		return
	}

	server()
}

func clientPre()(conn net.Conn, isClient bool, err error) {
    //打开连接:
    conn, err = net.Dial("tcp", "localhost:18080")
    if err != nil {
	isClient = false
        fmt.Println(err.Error())
        return
    }

    isClient = true

    conn.Close()
    return
}

func client(){
	sendContent := [2]string{"ping","pong"}
    conn, err := net.Dial("tcp", "localhost:18080")

    fmt.Println("client: ",conn.LocalAddr(),"server: ",conn.RemoteAddr()," ",rand.Intn(100)," ",sendContent[rand.Intn(100)%2])
	
	_,err = send(conn,sendContent[rand.Intn(100)%2])

	if err != nil {
        	fmt.Println(err.Error())
		return
	}
	
    doServerStuff(conn)
    return
}

func server() {
    listener, err := net.Listen("tcp", "0.0.0.0:18080")
    fmt.Println("server listen 18080")
    if err != nil {
        fmt.Println("开启端口错误,终止进程", err.Error())
        return //终止程序
    }
    // 监听并接受来自客户端的连接
    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("Error accepting", err.Error())
            return // 终止程序
        }

        go doServerStuff(conn)
    }
}

func doServerStuff(conn net.Conn) {
    count := 0
    recieveDefault := "ping"
    sendDefault := "pong"

    for {
	count += 1
	recieve,err := rev(conn)

        if err != nil {
            return 
        }

  	fmt.Printf("%d ", count)
  	if strings.EqualFold(recieve[0:len(recieveDefault)],recieveDefault) {
		go send(conn,sendDefault)
  	}else if strings.EqualFold(recieve[0:len(sendDefault)],sendDefault) {
		go send(conn,recieveDefault)
  	}
    }
}

func send(conn net.Conn, content string)(send string, err error){
	time.Sleep(100 * time.Millisecond)
        _, err = conn.Write([]byte(content))
	if err != nil {
		fmt.Println("发送错误.", err.Error())
		return
	}
	send = content

	fmt.Println("send", send)

	return
}

func rev(conn net.Conn)(recieve string, err error){
        buf := make([]byte, 512)
        _, err = conn.Read(buf)
        if err != nil {
	    fmt.Println("接收停止,原因:", err.Error())
            return 
        }

	recieve = string(buf)
  	fmt.Printf("recieving: %v ", recieve) 

	return
}

Rancher 上线应用商店的基本流程

在 Rancher 中,编写好 docker-commpose.yaml 和 rancher-compose.yaml 后,我们就可以部署应用了。 然而当我们给客户提供容器模板时候,更简单的方式还是走应用商店。

Rancher提供了一个应用商店,通过商店中的应用程序模版的可以简化部署复杂应用的过程。这篇文章简单记录一下创建私有应用商店的步骤。

可以查看 github 上的社区商店帮助理解:https://github.com/rancher/community-catalog

  • Cattle 调度引擎:templates文件夹
  • Swarm 调度引擎: swarm-templates文件夹
  • Mesos 调度引擎: mesos-templates文件夹

私有商店项目

-- templates (Or any of templates folder)
  |-- cloudflare
  |   |-- 0
  |   |   |-- docker-compose.yml
  |   |   |-- rancher-compose.yml
  |   |-- 1
  |   |   |-- docker-compose.yml
  |   |   |-- rancher-compose.yml
  |   |-- catalogIcon-cloudflare.svg
  |   |-- config.yml
...
  • 私有仓库至少要包含一个模板目录,明明为前文描述的那几个templates。这个目录确定 Rancher 的调用引擎。
  • 在这个templates目录下就放置我们的应用模板了。假设是 cloudflare
  • cloudflare文件夹中包含0、1、2等文件夹。第一个版本为0,后续每个版本加1。每增加一个新版本的文件夹,你就可以使用这个新版本的应用模版来升级应用了。另外,你也可直接更新0文件夹中的内容并重新部署应用。
  • cloudflare文件夹中还包含两个文件,一个是 config.yml,包含了应用模板的详细信息。另一个是模板的logo,以 catalogIcon- 开头。

以我的 hello world 为例,config.yml 的内容如下,单词的解释也蛮清楚得了:

name: Pingpong
description: kelu's ping pong hello world
version: v0.01
category: Entertainment
maintainer: kelvin blood <xxx@xxx.org>

将这个项目部署到 Rancher 可以访问的 git 服务器上,在 Rancher 设置中添加好就可以使用了。可以查看我的: https://github.com/kelvinblood/community-catalog/tree/init

Rancher 添加

在 Rancher 中一共有四种应用商店:

  • 个人应用商店
  • 共享应用商店
  • 社区应用商店
  • 官方应用商店

作为管理员可以将社区和官方应用商店关闭掉。如果以管理员身份添加的的应用商店,即为共享应用商店:

个人用户添加的应用商店入口在这:

参考资料


rancher-compose.yml

查了很多官方的文档,也没有见到 rancher-compose.yml reference. 可以从官方和社区的 rancher-compose.yml 里找到一些案例来学习一下。https://github.com/rancher/community-catalog

这个项目实际上是 Rancher Catelog 的项目。每个应用基本上就是 docker-compose.yml 和 rancher-compose.yml 构成。这两个文件定义了整个应用。同时我们也可以从老环境的应用中导出这两个文件,新环境直接部署即可使用,非常的便利。

询问了 Rancher 官方的开发者,他们的建议是理解概念以后使用 UI 界面进行配置后导出,不建议直接编写。

最简单的例子如下:

# Reference the service that you want to extend
version: '2'
services:
  web:
    scale: 2
  db:
    scale: 1

参考资料


美团点评业务之技术解密,日均请求数十亿次的容器平台 - infoq

转载自 InfoQ

本文介绍美团点评的 Docker 容器集群管理平台(以下简称容器平台)。该平台始于 2015 年,基于美团云的基础架构和组件而开发的 Docker 容器集群管理平台。目前该平台为美团点评的外卖、酒店、到店、猫眼等十几个事业部提供容器计算服务,承载线上业务数百个,容器实例超过 3 万个,日均线上请求超过 45 亿次,业务类型涵盖 Web、数据库、缓存、消息队列等等。

容器到来之前

在容器平台实施之前,美团点评的所有业务都是运行在美团私有云提供的虚拟机之上。美团点评大部分的线上业务都是面向消费者和商家的,业务类型多样,弹性的时间、频度也不尽相同,这对弹性服务提出了很高的要求。

在这一点上,虚拟机已经难以满足需求,主要体现以下两点:

  • 虚拟机弹性能力较弱,部署效率低,认为干预较多,可靠性差。

  • IT 成本高。由于虚拟机弹性能力较弱,业务部门为了应对流量高峰和突发流量,普遍采用预留大量机器和服务实例的做法。资源没有得到充分使用产生浪费。

容器时代

架构设计

美团点评将容器管理平台视作一种云计算模式,因此云计算的架构同样适用于容器。

如前所述,容器平台的架构依托于美团私有云现有架构,其中私有云的大部分组件可以直接复用或者经过少量扩展开发,如图所示。


360 的容器化之路

【编者的话】容器化技术作为“搅局者”,势必面临适配公司已有架构的挑战,本文将为大家介绍360如何让Docker落地。主要包括三方面内容:一,结合公司业务特点,如何使Docker适配现有技术架构 ,完成线上环境快速部署扩容;二,“让产品失败的更廉价”,使用Docker构建PaaS环境加速中小业务快速孵化上线;三,使用Docker技术,在构建持续集成环境方面的一些积累。

容器化技术作为“搅局者”,势必面临适配公司已有架构的挑战,本文将为大家介绍360如何让Docker落地。主要包括三方面内容:一,结合公司业务特点,如何使Docker适配现有技术架构 ,完成线上环境快速部署扩容;二,“让产品失败的更廉价”,使用Docker构建PaaS环境加速中小业务快速孵化上线;三,使用Docker技术,在构建持续集成环境方面的一些积累。

以Docker为主的容器化技术现在可谓风生水起,大家都觉得它可能会颠覆整个IT格局。我们刚开始接触到Docker的时候也觉得它非常好,有很多优点吸引我们。因为它的颠覆性我们称它为“搅局者”。

改造“搅局者”Docker

我们先来看看这位搅局者的优点:

  1. Namespace、CGroups虚拟化, 相比传统虚拟化会有更好性能,反映在生产环境中就是能更大程度的利用资源。
  2. 启动速度快,虚拟机最快也得30秒-1分钟,它的启动创建都是秒级。
  3. 镜像分层技术,解决了快速变更环境的问题。

这些优点很吸引我们,我们非常希望把它用在生产环境中,但是我们发现理想很美好,现实很残酷。我们之前基础架构都是使用传统虚拟机化技术就是虚拟机。我们要使用Docker就会面临这几个问题 :

  1. 不能SSH,紧急问题怎么排查?
  2. 怎么监控?
  3. 基础服务如何对接?
  4. 最重要的问题: 这东西稳定么,线上业务当然不能出问题。

所以,在应用Docker的时候,我们犯了犹豫,因为按照它推荐的方式,我们无法直接立马就在线上业务使用。因为Docker本身也对业务的架构设计有一定要求,比如我们常说的容器无状态,容器中不要留中间数据。我们发现公司的业务架构改造起来困难很大,涉及到方方面面,所以我们决定要Docker去适应公司的架构。

接下来我们就是要解决Docker技术”落地”的问题。

我们对Docker改造点主要有:

  1. 容器内部绑定独立IP。
  2. 容器内部开启多进程服务。
  3. 自动添加监控。
  4. CPU配额硬限制。
  5. 容器绑定独立IP这样外部可直接SSH了。

我们考虑在容器内部运行多个进程服务,因为默认容器只开启一个进程,这无法满足我们要求,所以我们大胆进行了改造。我们甚至在镜像里实现了chkconfig让以前的RPM包原生可用。

自动添加监控让创建的容器自动添加到Zabbix中。CPU配额硬限制 Docker 1.7版本已经支持了,我们在这之前自己实现了一套。

改造Docker支持这些功能后,我们又开发了一套调度系统,负责管理调度在集群上如何创建容器,我们也调研了一些开源的调度系统,发现都不满足需求,所以自己开发了一套。

通过这些手段我们就可以让Docker技术“落地”了,而带来的好处是,之前的体系我们要上线新的业务大约需要40分钟,使用Docker缩短到了5分钟。

这是分享的第一部分因为“搅局者”Docker使用遇到了困境,所以我们对它进行了一些改造,更好适配公司场景,让技术“落地”。

基于Docker做一个内部PaaS平台

紧接着我们基于Docker做了一个内部PaaS平台。公司每天会上线很多业务,这些业务有些是体量很大的重要业务,有些是带有试错性质的小业务。

传统业务上线的步骤会非常得严谨,流程会比较长,这些流程其实也对业务稳定性会有保障。有些试错性质的小业务,使用同样的流程变得不太合适,所以我们就想加速小业务上线流程,让他们可以快速上线,验证自己得价值。基于这种考虑,而且Docker天生的特点就特别适合干这个。

这是界面的一个截图,主要是前端Web UI去访问一个调度层 ,调度层通过调用Docker API来创建容器。目前PaaS平台支持PHP、Node.js、Python、Java等语言。

除了创建容器,我们还需要,创建Git仓库、配置访问代理等,总之研发一键就可以让业务进入待上线状态,只要他传完代码就可以上线了。

目前这个平台跑了300+业务,让很多研发只要有一个idea,就可以快速实施上线,很受他们欢迎。

这也是我们应用Docker的第二部分,通过私有PaaS平台,加速业务孵化。

关于持续集成

第三部分是关于持续集成。

持续集成当然是Docker最纯粹的玩法了,通过『Dockerfile-构建镜像-创建新容器』来完成环境的变更。

这块比较复杂,我们大致分了9个模块,比如调度模块、监控模块、存储模块等。

首先我们做了一个配置转换模块来转换Dockerfile,这样即可以统一镜像构建标准,同时也降低了编写Dockerfile的学习成本。

调度模块就直接用的Mesos和Marathon,镜像Registry直接使用了 Registry V2因为它性能更好对高并发支持也很好,最后是镜像构建模块,使用的是Jenkins CI。

但是我们发现一个问题:镜像构建在高并发下其实并不快。 比如装一个RPM包,SSH肯定会比重新build快得多。所以我们做了很多优化在镜像构建这一块,现在结果是100个任务同时构建我们也能达到和传统集群管理如Puppet一样的效率。

Q&A

问:开发自己的调度系统大概花了多久,有遇到特别的技术难点吗? 答:大约2个工程师一个月样子,没有太多得困难。因为调度逻辑比较简单。

问:您刚才说,通过绑定独立的IP就可以直接使用SSH了。 答:通过绑定独立的IP就可以直接使用SSH了,官方关于network那篇文档有介绍实现方案。

问:一般Docker的服务封装是no daemon的,这时如果重启服务,容器也会退出的,如何debug? 答:可使用supvirsod或者monit等将no daemon封装一下。

问:你们服务的注册发现用的是什么? 答:我们基础架构组开发了一个,名字叫QConf,已经开源在GitHub上

问:你说Zabbix做的一个容器监控,那有没有一个基于宿主机的监控方式?因为据我所知你这样的话每个容器都要运行一个代理吧。 答:我们就是使用宿主机里安装Zabbix代理,通过Zabbix自发现来动态获取容器列表,再基于自定义的监控脚本获得每个容器的监控数值。

问:你们的那些业务跑在Docker里了? 答:目前360的很多Web2.0业务已经跑在了上面,像影视、新闻、免费WIFI等。

问:Docker建议无状态,那么是否意味着不建议存放数据?比如MySQL,还是说通过-v来解决? 答: 这其实是数据存储问题,你可以使用分布式存储来存储数据,只要数据和逻辑分离容器就无状态了。

问:Docker建议无状态,那么是否意味着不建议存放数据?比如MySQL,还是说通过-v来解决? 答:我理解就是容器无状态就是基于镜像创建的马上就能线上使用。

问:线上Docker的稳定性如何? 答:目前运行都很稳定,没有出现容器异常崩溃等情况。

问:Container中跑多个进程,那么PID为1的进程你们是由什么控制的,直接由对应的应用程序还是其他什么? 答:之前用了supervisord 现在使用S6。

问:Registry面对大量的并发,有测试出大致的性能占比吗,整个registry是mirror还是其他架构? 答:Registry目前我们更新到了V2,我们测试V2在高并发pull和push上性能非常好,镜像存储使用共享存储,这样Registry也可以横向扩展。

问:如果容器配置用户可以直接访问的IP,在宿主虚拟机中是否可以基于Open vSwitch实现,否则会太依赖虚拟机网络? 答:这个可以的,实际上我们也测试过没问题,当时基于稳定性考虑没有使用。

问:奇虎的CPU配额管理是如何实现的? 答:这个Docker 1.7已经实现了,我们和官方的实现思路是一致的。

问:关于容器中数据存储是怎么做的,如果是共享存储如何进行对应? 答: 可以试试GlusterFS或者Ceph。

问:容器绑IP,容器重启后IP要重新绑吧,IP会变吗? 答:需要重新绑,可做成自动化脚本。