标签 docker 下的文章

TB一周萃选[第5期]

本文是首发于个人微信公众号的文章“TB一周萃选[第5期]”的归档。

img{512x368}

人生十鉴

大喜易失言
大怒易失礼
大惊易失态
大哀易失颜
大乐易失察
大惧易失节
大思易失爱
大醉易失德
大话易失信
大欲易失命

下雪,是北方城市冬天的“常规操作”,是最不需要被单独关注的的事情。但今年冬天的“雪”却成为了这边的热门话题,原因:自从入冬以来一直就没下一场像样儿的雪!

雪的姗姗来迟让病毒细菌异常活跃,医院发热门诊尤其是儿科人满为患,笔者入冬后也是连续感冒了两次。好在2018年元旦后没几天,在三九天到来之前,大家期盼已久的“像样儿的雪”终于落了下来。

img{512x368}
图:小区里的初雪

2018年的这第一场雪注定是一场“瑞雪”,它不仅降低了空气的病毒浓度,提升了空气湿度,帮助人们有效抵御病毒入侵人体,而且让缺雪的北方城市瞬间焕发出那冬天独有的“魅力”。

很多事情看起来很难,但一旦捅破了那层窗户纸之后,也就感觉没那么难了。下雪这事儿似乎也是如此,在被第一场雪打了个“样儿”之后,一场场雪便接踵而至了。在笔者撰写本文的时候,窗外还飞舞着洁白的雪花。

一、一周文章精粹

1. Go“不足够好”文章大集合

就像世界上其他事物一样,编程语言也没有完美的,每一门编程语言都有优点,也有“不够好”的地方。Go诞生以来,虽然赞美之声此起彼伏,但对Go的“批评”之声也从未中断过。因此有人就整理了Go“不足够好”文章大集合,供Go设计者反思,供Gopher学习,以更好地、更深刻地理解Go这门语言。

文章链接:“Go is not good enough”

2. 好的Go代码库应该具备的“特征”

Go是一门简洁的编程语言,入门容易,上手快。但写出好的Go代码还是需要一番功夫的。国外的一名gopher总结了“一个好的Go代码库应该具备的特征”,文章中按照依赖、API、错误处理、并发、调试等几个方面列举了诸如:给库打语义版本标签(semantic versioning tag)、除了标准库之外没有第三方依赖、一旦有非标准库的第三方依赖如何应对、不用使用vendor、使用包依赖管理工具、最小化public functions、接收iterface返回struct、避免创建goroutine、避免在公共API中使用channel等特征,强烈推荐每一个gopher阅读学习。

文章链接:好的Go代码库应该具备的“特征”

3. Apollo 2.0发布

在2018 美国消费电子展CES上,百度发布了其无人车平台Apollo的2.0版本,该版本将平台之前宣布的四大模块全部开放,并支持了简单城市道路的自动驾驶。

文章链接:Apollo 2.0

这是我之前写过的一篇文章Apollo 1.0的入门,可以帮助你了解Apollo。

img{512x368}

4. 2018,微服务将结束疯狂

微服务近两年在容器和k8s的赋能下迅速发展,成为架构师口中的“时尚词汇”,每每涉及系统设计,就会首先问是否要做成微服务。一个新事物的出现和发展,有人唱好,自然就会有人看衰。这不,“2018,微服务将结束疯狂”这篇文章就是给“微服务”泼冷水降温的!文章从微服务架构对开发者、运维人员、devops的影响、需要专家级技能、真实世界系统边界模糊、状态复杂性、通信复杂性、版本管理、分布式事务等方面探讨了微服务的劣势,并给出了一个问题列表,建议大家在决定采用微服务之前,用这些问题问问自己,以避免陷入“微服务”泥潭中去。

文章链接:《The Death of microservice madness in 2018》

5. 2017 Google Brain Team的总结 by Jeff Dean

在人工智能的工程领域,Google大神Jeff Dean领导的Brain Team具有举足轻重的地位,也可以说是世界上最好的人工智能实践和研究团队之一了。在2018年伊始,Jeff Dean代表Google Brain Team撰文对团队在2017年的工作及成果进行了总结:包括AutoML、语言理解、机器学习算法、机器学习系统等核心研究工作,以及开源软件Tensorflow、数据集和新的机器学习硬件TPU等方面的最新进展。 对非人工智能领域而言,文章中满满的都是“黑科技”啊,能真正看懂文章中这些内容的朋友你一定也是人工智能领域的大牛了。

img{512x368}
图:Tensorflow用户分布

文章链接:
“2017 Google Brain Team的总结- part1″
“2017 Google Brain Team的总结- part2″
Part1 中文版

6. Javascript工作原理

Javascript诞生之后,估计没人想到过js能像今天这么流行:统治了前端,渗透到了后端,并成为后端服务开发的重要技术栈之一。Js语言也十分简单,但外延也很大,你要至少要深入理解浏览器原理才能更好地发挥JS的威力。sessionstack公司官方blog曾发表了几篇有关Javascript工作原理的文章,可以系统地帮助Javascript了解Js的运行机制。

img{512x368}

文章链接:
Javascript工作原理: 引擎、运行时与调用栈
Javascript工作原理: V8引擎和优化技巧
Javascript工作原理: 内存管理与避免内存泄露的技巧
Javascript工作原理: event loop与async编程

二、一周资料分享

1. 斯坦福大学面向Tensorflow深度学习研究课程

欧美一流大学在计算机技术方面的“与时俱进”的能力与速度真的是十分值得我们学习和借鉴的,尤其是斯坦福这样靠近硅谷的大学,其技术课程更新的速度非常快。Tensorflow于2015年末开源,2017年2月正式发布1.0版本。斯坦福大学在2017年就开了一门新课:“CS 20SI: Tensorflow for Deep Learning Research”,教授学生如何使用Tensorflow进行深度学习研究。

这门课程涵盖了用于深入学习研究的Tensorflow基本原理和使用用法。 目标是帮助学生们理解TensorFlow的graphical computational model,探索它提供的各种功能,并学习如何构建最适合深度学习项目的模型。 课程中学生将使用TensorFlow建立不同复杂度的模型,从简单的线性/逻辑回归到卷积神经网络和递归神经网络,解决词嵌入,单词翻译,光学字符识别,强化学习等任务。 学生还将学习到构建模型和研究实验管理的最佳实践。

资料链接:
CS 20SI: Tensorflow for Deep Learning Research 课程主页
CS 20SI: Tensorflow for Deep Learning Research 2017课程归档

三、一周工具推荐

1. stackedit

自从我的博客转到使用Markdown格式进行编辑后,我就一直使用stackedit提供的在线所见即所得的Markdown编辑器进行内容的编辑。最初的stackedit v4表现的还不强大,随着stackedit v5在线版本的推出,stackedit已经可以满足绝大多数Markdown编辑的功能需求了。

img{512x368}

  • 支持在线/离线管理多个markdown文件
  • 支持多种文件格式导出,包括HTML、PDF、WORD、EPUB
  • 支持文件的云同步,支持Google Drive, Dropbox等主流云存储系统
  • 支持将Markdown直接上传到Blogger/Blogspot, WordPress, Zendesk
  • 支持将Markdown直接发布到GitHub, Gist, Google Drive, Dropbox
    … …

更难得的是stackedit还是一个受关注度极高的开源项目(stars over 1w),你可以自己本地部署一个专用的stackedit。

工具链接:stackedit.io
工具开源项目链接:stackedit

四、一周图书推荐

1.《演讲模式:演讲的技巧与禁忌》

img{512x368}

技术人升级到一定level后,可能少不了要做些演讲、做些培训之类的活动。但对多数技术人而言,演讲这事并不是“舒适区”范围内。市面上有关介绍演讲技巧方面的书可谓是“汗牛充栋”,但这Neal Ford领衔编写的这本书《演讲模式》却独具特色。Neal Ford是Thoughtworks的大神,其他两位作者也是IT圈中的牛人。与其他作者相比,他们更熟悉IT技术人的思维方式,他们也以IT人独特的思维方式,创造性地将建筑和软件开发领域“模式”的概念引入演讲领域,围绕演讲的全过程总结了能迅速有效提升演讲技能的88个模式(应该掌握的技巧)和反模式(应该避免的不好的做法)。对于IT人而言,“模式”这词再熟悉不过了,因此这本书更像是IT技术人员间地手把手地传道解惑。

图书链接:
中文版:《演讲模式:演讲的技巧与禁忌》
英文版:《Presentation Patterns: Techniques for Crafting Better Presentations》


我的联系方式:

微博:http://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

写Go代码时遇到的那些问题[第1期]

程序员步入“大龄”,写代码的节奏也会受到影响。以前是长时间持续地写,现在写代码的节奏变成了“波浪形”:即写一段时间,歇一段时间。当然这里的“歇”并不是真的歇,而是做其他事情了,比如:回顾、整理与总结。

平时写Go代码,时不时就遇到一些问题,或是写出一些让自己还算满意的代码,这里全部列为“问题”行列。这些“问题”(以及其解决方法)往往比较“小”、比较“碎片”,不适合以自己“擅长”的“长篇”风格写出来分享,也不知道以什么样的“题目”去分享更好,但这样的“问题”在日常又总是会遇到。考量来考量去,赶脚还是用一系列的文章去分享比较合适,即每隔一段时间,积累了一些问题后,就写一篇文章分享一下。

这是第一篇,后续不确定时间地(注意:这不是weekly的哦)发布新篇,直到没啥可写了或不写Go代码了^0^。

一、Go包管理

首当其冲的是Go包管理

1. vendor的“传染性”带来的问题

Go从1.5版本开始引入vendor机制以辅助Go的包管理。随着vendor机制的应用日益广泛,我们会发现:有些时候你要是不用vendor(在不借助第三方包管理工具的前提下),很多编译问题是解决不了的,或者说vendor机制有一定的传染性。比如下面这个例子:

img{512x368}

如上图所示:app_c包直接调用lib_a包中函数,并使用了lib_b包(v0.2版本)中的类型,lib_a包vendor了lib_b包(v0.1版本)。在这样的情况下,当我们编译app_c包时,是否会出现什么问题呢?我们一起来看一下这个例子:

在$GOPATH/src路径下面我们查看当前示例的目录结构:

$tree
├── app_c
    ├── c.go
├── lib_a
    ├── a.go
    └── vendor
        └── lib_b
            └── b.go
├── lib_b
    ├── b.go

各个源文件的示例代码如下:

//lib_a/a.go
package lib_a

import "lib_b"

func Foo(b lib_b.B) {
    b.Do()
}

//lib_a/vendor/lib_b/b.go

package lib_b

import "fmt"

type B struct {
}

func (*B) Do() {
    fmt.Println("lib_b version:v0.1")
}

// lib_b/b.go
package lib_b

import "fmt"

type B struct {
}

func (*B) Do() {
    fmt.Println("lib_b version:v0.2")
}

// app_c/c.go
package app_c

import (
    "lib_a"
    "lib_b"
)

func main() {
    var b lib_b.B
    lib_a.Foo(b)
}

进入app_c目录,执行编译命令:

$go build c.go
# command-line-arguments
./c.go:10:11: cannot use b (type "lib_b".B) as type "lib_a/vendor/lib_b".B in argument to lib_a.Foo

我们看到go compiler认为:app_c包main函数中定义的变量b的类型(lib_b.B)与lib_a.Foo的参数b的类型(lib_a/vendor/lib_b.B)是不同的类型,不能相互赋值

2. 通过手工vendor解决上述问题

这个例子非常有代表性,那么怎么解决这个问题呢?我们需要在app_c中也使用vendor机制,即将app_c所需的lib_a和lib_b都vendor到app_c中。

按照上述思路解决后的示例的目录结构:

$tree
├── app_c
    ├── c.go
    └── vendor
        ├── lib_a
        │   └── a.go
        └── lib_b
            └── b.go
├── lib_a
    ├── a.go
    └── vendor
        └── lib_b
            └── b.go
├── lib_b
    ├── b.go

不过要注意的是:app_c/vendor下面的库中的vendor目录要被删除掉的,我们只保留顶层vendor。现在我们再来编译c.go就可以顺利编译通过了。

3. 使用dep

对于demo或规模不大、依赖不多的小项目,手工进行vendor还是蛮有效的。一个可行的手工vendor步骤:

  • 在项目顶层创建vendor;
  • 通过go list -json ./…查看项目依赖 “deps”;
  • 逐一下载各个依赖,并确定要使用的版本(tag or branch),将特定版本cp到顶层的vendor目录下,至少要做到vendor所有直接依赖包;
  • 可以在顶层vendor下创建dependencies.list文件,手工记录vendor的依赖包列表以及版本信息。

但是对于稍大一点的项目,手工vendor就会费时费力,有时仅能顾及到“直接依赖包”的vendor,“数不清”的间接依赖/传递依赖会让你头疼不已。这个时候我们会想到使用第三方的包管理工具。在现在这个时间点,如果你再和我提godepglide等,那你就out了,dep是首选。

《初窥dep》一文中,我们对当时的dep进行了较为详细的工作机制分析,如今dep已经演化到0.3.2版本了,并且commandline交互接口已经稳定了。dep init默认采用network mode,即到各个依赖包的upstream上查找版本信息并下载;dep init也支持-gopath模式,即在本地$GOPATH下获取依赖包的元信息并分析。

不过,对于在国内的gopher,dep init的过程依然是一道很难逾越的“坎”。问题多出在:第三方包特别喜欢依赖的golang.org/x下的那些包,常见的包有:net、text、crypto等。golang.org/x/{package_name}仅仅是canonical import path,真正的代码存储在go.googlesource.com上,而在国内get这些包,我们会得到如下错误:

$go get -u golang.org/x/net
package golang.org/x/net: unrecognized import path "golang.org/x/net" (https fetch: Get https://golang.org/x/net?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)

这将导致dep init命令长期阻塞,给国内gopher带来极为糟糕的体验。更为糟糕的是,即便是采用了一些fan qiang方式,有些时候go.googlesource.com依旧无法连接。因此,我一般的作法是在国外的主机上进行dep init,然后将vendor checkin到代码仓库中。这样其他人在得到你的代码后,也不需dep ensure(也要下载依赖包)即可实现reproducable build。

有些朋友可能会将从github.com/golang上下载的net包来代替golang.org/x/net,并使用dep init -v -gopath=true的模式。但这种替换会被dep分析出来,因为dep会尝试去读取代码库的元信息,结果依然会是失败。

二. 非容器化应用的本地日志管理

微服务、容器化大行其道的今天,单个应用的日志处理变得简单化了,应用只需要将要输出的信息输出到stdout、stderr上即可。logging基础设施会收集容器日志,并做后续归档、分析、过滤、查找、展示等处理。但是在非容器环境、在没有统一的logging基础设施的前提下,日志的管理就又交还给应用自身了。浅显的日志管理至少要包含日志的rotate(轮转)、压缩归档以及历史归档文件的处理吧。这里我们就来探讨一下这个问题的几种解决方法。

1. 托管给logrotate

在主流的Linux发行版上都有一个logrotate工具程序,应用程序可以借助该工具对应用输出的日志进行rotate、压缩、归档和删除历史归档日志,这样可大幅简化应用的日志输出逻辑,应用仅需要将日志输出到一个具名文件中即可,其余都交给logrotate处理。

我们建立一个输出log的demo app:

//testlogrotate.go

package main

import (
    "log"
    "os"
    "time"
)

func main() {
    file, err := os.OpenFile("./app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
        log.Fatalln("Failed to open log file:", err)
    }
    defer file.Close()

    logger := log.New(file,
        "APP_LOG_PREFIX: ",
        log.Ldate|log.Ltime|log.Lshortfile)

    for {
        logger.Println("test log")
        time.Sleep(time.Second * 1)
    }
}

该程序每隔1s向app.log文件写入一行日志。

# tail -f app.log
APP_LOG_PREFIX: 2018/01/12 19:14:43 testlogrotate.go:22: test log
APP_LOG_PREFIX: 2018/01/12 19:14:44 testlogrotate.go:22: test log
APP_LOG_PREFIX: 2018/01/12 19:14:45 testlogrotate.go:22: test log
APP_LOG_PREFIX: 2018/01/12 19:14:46 testlogrotate.go:22: test log
APP_LOG_PREFIX: 2018/01/12 19:14:47 testlogrotate.go:22: test log
... ..

接下来,我们就要用logrotate对该app.log文件进行定期的rotate、压缩归档以及历史归档清理了,我们需要为app.log定制一个配置。logrotate读取配置的目录是/etc/logrotate.d,我们在/etc/logrotate.d下面建立applog文件(当然你也可以在任意其他目录下建立配置文件,不过其他目录下的配置文件无法被logrotate的cron任务感知到,不过这样的配置文件可以手工与logrotate程序结合使用),文件内容如下:

# cat /etc/logrotate.d/applog

/data/tonybai/test/go/app.log {
  rotate 7
  daily
  size=10M
  compress
  dateext
  missingok
  copytruncate
}

这个配置的大致含义是:
* 每天rotate一次
* 日志保留7天(rotate=7, daily rotate)
* 归档日志采用压缩形式
* 归档日志带有时间戳
* 当当前日志size > 10M时,会进行一次rotate
* 最重要的是copytruncate这个配置,这个配置的含义是将app.log当前日志copy到一个归档文件后,对app.log进行truncate操作,这样app.log的open file fd并不改变,不会影响到原app继续写日志。当然这个copy的过程中可能会有少量日志lost。

如果你觉得logrotate在时间粒度和精确度上依旧无法满足你的要求,你可以结合crontab自己定时执行logrotate(crontab -e编辑crontab的配置):

# logrotate -f /etc/logrotate.d/applog

下面是rotate时,tail -f中看到的情况:

APP_LOG_PREFIX: 2018/01/12 20:25:59 testlogrotate.go:21: test log
APP_LOG_PREFIX: 2018/01/12 20:26:00 testlogrotate.go:21: test log
tail: app.log: file truncated
APP_LOG_PREFIX: 2018/01/12 20:26:01 testlogrotate.go:21: test log
APP_LOG_PREFIX: 2018/01/12 20:26:02 testlogrotate.go:21: test log
APP_LOG_PREFIX: 2018/01/12 20:26:03 testlogrotate.go:21: test log

可以看到tail可以检测到file truncate事件。

2. 使用自带rotate功能log包

在go技术栈中众多的logging包中,logrus是使用较为广泛的一个包,支持与std库 log API兼容的结构化日志、支持logging level设置、支持安全地并发写日志以及hook等。但logrus自身并不具备auto rotate功能,需要结合其他工具才能实现。这里用nate finchlumberjack,我们来看一个简单的例子:

// testlogrusAndlumberjack.go

package main

import (
    "time"

    "github.com/natefinch/lumberjack"
    log "github.com/sirupsen/logrus"
)

func main() {
    logger := log.New()
    logger.SetLevel(log.DebugLevel)
    logger.Formatter = &log.JSONFormatter{}

    logger.Out = &lumberjack.Logger{
        Filename:   "./app.log",
        MaxSize:    1, // megabytes
        MaxBackups: 3,
        MaxAge:     1,    //days
        Compress:   true, // disabled by default
        LocalTime:  true,
    }

    for {
        logger.Debug("this is an app log")
        time.Sleep(2 * time.Millisecond)
    }
}

从代码里,我们看到:通过设置logger.Out为一个lumberjack.Logger的实例,将真正的Write交给了lumberjack.Logger,而后者实现了log的rotate功能,与logrotate的配置有些类似,这里也包括日志最大size设定、保留几个归档日志、是否压缩、最多保留几天的日志。不过当前lumberjack实现的rotate判断条件仅有一个:MaxSize,而没有定时rotate的功能。

我们执行一下该程序,等待一会,并停止程序。可以看到目录下的日志文件发生了变化:

$ls -lh
-rw-r--r--  1 tony  staff   3.7K Jan 12 21:03 app-2018-01-12T21-03-42.844.log.gz
-rw-r--r--  1 tony  staff   3.7K Jan 12 21:04 app-2018-01-12T21-04-15.017.log.gz
-rw-r--r--  1 tony  staff   457K Jan 12 21:04 app.log

lumberjack每发现app.log大于MaxSize就会rotate一次,这里已经有了两个归档压缩文件,并被lumberjack赋予了时间戳和序号,便于检索和查看。

3. 关于对日志level的支持以及loglevel的热更新

对日志level的支持是logging包选项的一个重要参考要素。logrus支持设置六个log level:

    PanicLevel
    FatalLevel
    ErrorLevel
    WarnLevel
    InfoLevel
    DebugLevel

并且对不同的leve的日志,logrus支持设定hook分别处理,比如:放到不同的日志文件中。通过logrus.Logger.SetLevel方法可以运行时更新logger实例的loglevel,这个特性可以让我们在生产环境上通过临时打开debuglevel日志对程序进行更细致的观察,以定位问题,快速定位bug,非常实用。

结合系统Signal机制,我们可以通过USR1和USR2两个signal来运行时调整程序的日志级别,我们来看一个示例:

img{512x368}

从上面图片可以看到,日志级别从高到低分别为:Panic, Fatal, Error, Warn,Info和Debug。如果要调高log level,我们向程序发送USR1来调高日志级别,相反,发送USR2来调低日志级别:

我们在testlogrusAndlumberjack.go上面做些修改:增加对signal: USR1和USR2的监听处理,同时循环打印各种级别日志,以后续验证日志级别的动态调整:

// testloglevelupdate.go

import (
    log "github.com/sirupsen/logrus"
    ... ...
)

func main() {
    logger := log.New()
    logger.SetLevel(log.DebugLevel)
    logger.Formatter = &log.JSONFormatter{}

    logger.Out = &lumberjack.Logger{
        Filename:   "./app.log",
        MaxSize:    1, // megabytes
        MaxBackups: 3,
        MaxAge:     1,    //days
        Compress:   true, // disabled by default
        LocalTime:  true,
    }

    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGUSR1, syscall.SIGUSR2)
    go watchAndUpdateLoglevel(c, logger)

    for {
        logger.Debug("it is debug level log")
        logger.Info("it is info level log")
        logger.Warn("it is warning level log")
        logger.Error("it is warning level log")
        time.Sleep(5 * time.Second)
    }
}

watchAndUpdateLoglevel函数用于监听程序收到的系统信号,并根据信号类型调整日志级别:

// testloglevelupdate.go
func watchAndUpdateLoglevel(c chan os.Signal, logger *log.Logger) {
    for {
        select {
        case sig := <-c:
            if sig == syscall.SIGUSR1 {
                level := logger.Level
                if level == log.PanicLevel {
                    fmt.Println("Raise log level: It has been already the most top log level: panic level")
                } else {
                    logger.SetLevel(level - 1)
                    fmt.Println("Raise log level: the current level is", logger.Level)
                }

            } else if sig == syscall.SIGUSR2 {
                level := logger.Level
                if level == log.DebugLevel {
                    fmt.Println("Reduce log level: It has been already the lowest log level: debug level")
                } else {
                    logger.SetLevel(level + 1)
                    fmt.Println("Reduce log level: the current level is", logger.Level)
                }

            } else {
                fmt.Println("receive unknown signal:", sig)
            }
        }
    }
}

运行该程序后,你可以通过如下命令向程序发送信号:

$ kill -s USR1|USR2 程序的进程号

通过日志的输出,可以判断出日志级别调整是否生效,这里就不细说了。

不过这里还要提一点的是logrus目前对于输出的日志中双引号内的一些字符(比如双引号自身)会做转义处理,即在前面加上“反斜杠”,比如:

{"level":"debug","msg":"receive a msg: {\"id\":\"000002\",\"ip\":\"201.108.111.117\"}","time":"2018-01-11T20:42:31+08:00"}

这个问题让日志可读性大幅下降,但这个问题似乎尚处于无解状态

三. json marshal json string时的转义问题

之前写过这样一个function,用于统一marshal内部组件通信的应答消息:

func marshalResponse(code int, msg string, result interface{}) (string, error) {
    m := map[string]interface{}{
        "code":   0,
        "msg":    "ok",
        "result": result,
    }

    b, err := json.Marshal(&m)
    if err != nil {
        return "", err
    }

    return string(b), nil
}

不过当result类型为json string时,这个函数的输出带有转义反斜线:

//testmarshaljsonstring.go
... ...
func main() {
    s, err := marshalResponse(0, "ok", `{"name": "tony", "city": "shenyang"}`)
    if err != nil {
        fmt.Println("marshal response error:", err)
        return
    }
    fmt.Println(s)
}

运行这个程序输出:

{"code":0,"msg":"ok","result":"{\"name\": \"tony\", \"city\": \"shenyang\"}"}

怎么解决掉这个问题呢?json提供了一种RawMessage类型,本质上就是[]byte,我们将json string转换成RawMessage后再传给json.Marshal就可以解决掉这个问题了:

//testmarshaljsonstring.go
func marshalResponse1(code int, msg string, result interface{}) (string, error) {
    s, ok := result.(string)
    var m = map[string]interface{}{
        "code": 0,
        "msg":  "ok",
    }

    if ok {
        rawData := json.RawMessage(s)
        m["result"] = rawData
    } else {
        m["result"] = result
    }

    b, err := json.Marshal(&m)
    if err != nil {
        return "", err
    }

    return string(b), nil
}

func main() {
    s, err = marshalResponse1(0, "ok", `{"name": "tony", "city": "shenyang"}`)
    if err != nil {
        fmt.Println("marshal response1 error:", err)
        return
    }
    fmt.Println(s)
}

再运行这个程序的输出结果就变成了我们想要的结果了:

{"code":0,"msg":"ok","result":{"name":"tony","city":"shenyang"}}

四. 如何在main包之外使用flag.Parse后的命令行flag变量

我们在使用Go开发交互界面不是很复杂的command-line应用时,一般都会使用std中的flag包进行命令行flag解析,并在main包中校验和使用flag.Parse后的flag变量。常见的套路是这样的:

//testflag1.go
package main

import (
    "flag"
    "fmt"
)

var (
    endpoints string
    user      string
    password  string
)

func init() {
    flag.StringVar(&endpoints, "endpoints", "127.0.0.1:2379", "comma-separated list of etcdv3 endpoints")
    flag.StringVar(&user, "user", "", "etcdv3 client user")
    flag.StringVar(&password, "password", "", "etcdv3 client password")
}

func usage() {
    fmt.Println("flagdemo-app is a daemon application which provides xxx service.\n")
    fmt.Println("Usage of flagdemo-app:\n")
    fmt.Println("\t flagdemo-app [options]\n")
    fmt.Println("The options are:\n")

    flag.PrintDefaults()
}

func main() {
    flag.Usage = usage
    flag.Parse()

   // ... ...
   // 这里我们可以使用endpoints、user、password等flag变量了
}

在这样的一个套路中,我们可以在main包中直接使用flag.Parse后的flag变量了。但有些时候,我们需要在main包之外使用这些flag vars(比如这里的:endpoints、user、password),怎么做呢,有几种方法,我们逐一来看看。

1. 全局变量法

我想大部分gopher第一个想法就是使用全局变量,即建立一个config包,包中定义全局变量,并在main中将这些全局变量绑定到flag的Parse中:

$tree globalvars
globalvars
├── config
│   └── config.go
├── etcd
│   └── etcd.go
└── main.go

// flag-demo/globalvars/config/config.go

package config

var (
    Endpoints string
    User      string
    Password  string
)

// flag-demo/globalvars/etcd/etcd.go
package etcd

import (
    "fmt"

    "../config"
)

func EtcdProxy() {
    fmt.Println(config.Endpoints, config.User, config.Password)
    //... ....
}

// flag-demo/globalvars/main.go
package main

import (
    "flag"
    "fmt"
    "time"

    "./config"
    "./etcd"
)

func init() {
    flag.StringVar(&config.Endpoints, "endpoints", "127.0.0.1:2379", "comma-separated list of etcdv3 endpoints")
    flag.StringVar(&config.User, "user", "", "etcdv3 client user")
    flag.StringVar(&config.Password, "password", "", "etcdv3 client password")
}

.... ...

func main() {
    flag.Usage = usage
    flag.Parse()

    go etcd.EtcdProxy()

    time.Sleep(5 * time.Second)
}

可以看到,我们在绑定cmdline flag时使用的是config包中定义的全局变量。并且在另外一个etcd包中,使用了这些变量。

我们运行这个程序:

./main -endpoints 192.168.10.69:2379,10.10.12.36:2378 -user tonybai -password xyz123
192.168.10.69:2379,10.10.12.36:2378 tonybai xyz123

不过这种方法要注意这些全局变量值在Go包初始化过程的顺序,比如:如果在etcd包的init函数中使用这些全局变量,那么你得到的各个变量值将为空值,因为etcd包的init函数在main.init和main.main之前执行,这个时候绑定和Parse都还未执行。

2. 传参法

第二种比较直接的想法就是将Parse后的flag变量以参数的形式、以某种init的方式传给其他要使用这些变量的包。

$tree parampass
parampass
├── etcd
│   └── etcd.go
└── main.go

// flag-demo/parampass/etcd/etcd.go
package etcd
... ...

func EtcdProxy(endpoints, user, password string) {
    fmt.Println(endpoints, user, password)
}

// flag-demo/parampass/main.go
package main

import (
    "flag"
    "fmt"
    "time"

    "./etcd"
)

var (
    endpoints string
    user      string
    password  string
)

func init() {
    flag.StringVar(&endpoints, "endpoints", "127.0.0.1:2379", "comma-separated list of etcdv3 endpoints")
    flag.StringVar(&user, "user", "", "etcdv3 client user")
    flag.StringVar(&password, "password", "", "etcdv3 client password")
}

... ...

func main() {
    flag.Usage = usage
    flag.Parse()

    go etcd.EtcdProxy(endpoints, user, password)

    time.Sleep(5 * time.Second)
}

这种方法非常直观,这里就不解释了。但注意:一旦使用这种方式,一定需要在main包与另外的包之间建立某种依赖关系,至少main包会import那些使用flag变量的包。

3. 配置中心法

全局变量法直观,而且一定程度上解除了其他包与main包的耦合。但是有一个问题,那就是一旦flag变量发生增减,config包就得相应添加或删除变量定义。是否有一种方案可以在flag变量发生变化时,config包不受影响呢?我们可以用配置中心法。所谓的配置中心法,就是实现一个与flag变量类型和值无关的通过配置存储结构,我们在main包中向该结构注入parse后的flag变量,在其他需要flag变量的包中,我们使用该结构得到flag变量的值。

$tree configcenter
configcenter
├── config
│   └── config.go
└── main.go

//flag-demo/configcenter/config/config.go
package config

import (
    "log"
    "sync"
)

var (
    m  map[string]interface{}
    mu sync.RWMutex
)

func init() {
    m = make(map[string]interface{}, 10)
}

func SetString(k, v string) {
    mu.Lock()
    m[k] = v
    mu.Unlock()
}

func SetInt(k string, i int) {
    mu.Lock()
    m[k] = i
    mu.Unlock()
}

func GetString(key string) string {
    mu.RLock()
    defer mu.RUnlock()
    v, ok := m[key]
    if !ok {
        return ""
    }
    return v.(string)
}

func GetInt(key string) int {
    mu.RLock()
    defer mu.RUnlock()
    v, ok := m[key]
    if !ok {
        return 0
    }
    return v.(int)
}

func Dump() {
    log.Println(m)
}

// flag-demo/configcenter/main.go

package main

import (
    "flag"
    "fmt"
    "time"

    "./config"
)

var (
    endpoints string
    user      string
    password  string
)

func init() {
    flag.StringVar(&endpoints, "endpoints", "127.0.0.1:2379", "comma-separated list of etcdv3 endpoints")
    flag.StringVar(&user, "user", "", "etcdv3 client user")
    flag.StringVar(&password, "password", "", "etcdv3 client password")
}
... ...
func main() {
    flag.Usage = usage
    flag.Parse()

    // inject flag vars to config center
    config.SetString("endpoints", endpoints)
    config.SetString("user", user)
    config.SetString("password", password)

    time.Sleep(5 * time.Second)
}

我们在main中使用config的SetString将flag vars注入配置中心。之后,我们在其他包中就可以使用:GetString、GetInt获取这些变量值了,这里就不举例了。

4、“黑魔法”: flag.Lookup

flag包中提供了一种类似上述的”配置中心”的机制,但这种机制不需要我们显示注入“flag vars”了,我们只需按照flag提供的方法在其他package中读取对应flag变量的值即可。

$tree flaglookup
flaglookup
├── etcd
│   └── etcd.go
└── main.go

// flag-demo/flaglookup/main.go
package main

import (
    "flag"
    "fmt"
    "time"

    "./etcd"
)

var (
    endpoints string
    user      string
    password  string
)

func init() {
    flag.StringVar(&endpoints, "endpoints", "127.0.0.1:2379", "comma-separated list of etcdv3 endpoints")
    flag.StringVar(&user, "user", "", "etcdv3 client user")
    flag.StringVar(&password, "password", "", "etcdv3 client password")
}

......

func main() {
    flag.Usage = usage
    flag.Parse()

    go etcd.EtcdProxy()

    time.Sleep(5 * time.Second)
}

// flag-demo/flaglookup/etcd/etcd.go
package etcd

import (
    "flag"
    "fmt"
)

func EtcdProxy() {
    endpoints := flag.Lookup("endpoints").Value.(flag.Getter).Get().(string)
    user := flag.Lookup("user").Value.(flag.Getter).Get().(string)
    password := flag.Lookup("password").Value.(flag.Getter).Get().(string)

    fmt.Println(endpoints, user, password)
}

运行该程序:

$go run main.go -endpoints 192.168.10.69:2379,10.10.12.36:2378 -user tonybai -password xyz123
192.168.10.69:2379,10.10.12.36:2378 tonybai xyz123

输出与我们的预期是一致的。

5、对比

我们用一幅图来对上述几种方法进行对比:

img{512x368}

很显然,经过简单包装后,“黑魔法”flaglookup应该是比较优异的方案。main包、other packages只需import flag即可。

注意:在main包中定义exported的全局flag变量并被其他package import的方法是错误的,很容易造成import cycle问题。并且任何其他package import main包都是不合理的

五. 小结

以上是这段时间遇到的、收集的一些Go问题以及solution。注意:这些solution不一定是最优方案哦!如果您有更好方案,欢迎批评指正和互动交流

本文章中涉及到的所有源码和配置文件在这里可以下载到。


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

微信赞赏:
img{512x368}

使用istio治理微服务入门

近两年微服务架构流行,主流互联网厂商内部都已经微服务化,初创企业虽然技术积淀不行,但也通过各种开源工具拥抱微服务。再加上容器技术赋能,Kubernetes又添了一把火,微服务架构已然成为当前软件架构设计的首选。

但微服务化易弄,服务治理难搞!

一、微服务的“痛点”

微服务化没有统一标准,多数是进行业务领域垂直切分,业务按一定的粒度划分职责,并形成清晰、职责单一的服务接口,这样每一块规划为一个微服务。微服务之间的通信方案相对成熟,开源领域选择较多的有RPC或RESTful API方案,比如:gRPCapache thrift等。这些方案多偏重于数据如何打包、传输与解包,对服务治理的内容涉及甚少。

微服务治理是头疼的事,也是微服务架构中的痛点治理这个词有多元含义,很难下达一个精确定义,这里可以像小学二年级学生那样列出治理的诸多近义词:管理、控制、规则、掌控、监督、支配、规定、统治等。对于微服务而言,治理体现在以下诸多方面:

  • 服务注册与发现
  • 身份验证与授权
  • 服务的伸缩控制
  • 反向代理与负载均衡
  • 路由控制
  • 流量切换
  • 日志管理
  • 性能度量、监控与调优
  • 分布式跟踪
  • 过载保护
  • 服务降级
  • 服务部署与版本升级策略支持
  • 错误处理
  • … …

从微服务治理角度来说,微服务其实是一个“大系统”,要想将这个大系统全部落地,绝非易事,尤其是之前尚没有一种特别优雅的技术方案。多数方案(比如:dubbogo-kit等。)都或多或少地对应用逻辑有一定的侵入性,让业务开发人员不能只focus到业务本身,还要关心那些“治理”逻辑。并且市面上内置了微服务治理逻辑的框架较少,且很多编程语言相关。这种情况下,大厂多选择自研或基于某个框架改造,小厂一般只能“东拼西凑”一些“半成品”凑合着使用,就这样微服务也走过了若干年。

二、Service Mesh横空出世,istio带来“福音”

我不知道在没有TCP/IP协议的年代,主机和主机之间的应用通信时是否需要应用关心底层通信协议实现逻辑。但是和TCP/IP诞生的思想类似,在微服务使用多年后,人们发现需要独立地抽象出一层逻辑网络,专门用于“微服务通信与治理策略的落地”,让应用只关心业务,把服务治理的事情全部交由“这一层”去处理。

img{512x368}
图:传统微服务之间的微服务治理逻辑的位置

img{512x368}
图:微服务治理逻辑被独立出来之后的位置

由“Service Govern Logic”这一层组成的逻辑网络被定义为service mesh,每个微服务都包含一个service mesh的端点。

“Service Mesh”概念还非常年轻,这个词在国内被翻译为“服务网格”或“服务啮合层”,我们这里就用Service Mesh这个英文词。这里摘录一下ServiceMesh中文社区上的一篇名为“年度盘点2017之Service Mesh:群雄逐鹿烽烟起”的文章中对Service Mesh概念的回顾:

  • 在 2016 年年初,“Service Mesh”还只是 Buoyant 公司的内部词汇,而之后,它开始逐步走向社区:
  • 2016 年 9 月 29 日在 SF Microservices 上,“Service Mesh”这个词汇第一次在公开场合被使用。这标志着“Service Mesh”这个词,从 Buoyant 公司走向社区。
  • 2016 年 10 月,Alex Leong 开始在 Buoyant 公司的官方 Blog 中连载系列文章“A Service Mesh for Kubernetes”。随着“The Services must Mesh”口号的喊出,Buoyant 和 Linkerd 开始 Service Mesh 概念的布道。
  • 2017 年 4 月 25 日,William Morgan 发布博文“What’s a service mesh? And why do I need one?”。正式给 Service Mesh 做了一个权威定义。

而Service Mesh真正引起大家关注要源于istio项目的开源发布。为什么呢?个人觉得还是因为“爹好”!istio项目由Google、IBM共同合作创建,lyft公司贡献了envoy项目将作为istio service mesh的data panel。Google、IBM的影响力让Service Mesh概念迅速传播,同时也让大家认识到了istio项目在service mesh领域的重要性,于是纷纷选择积极支持并将自己的产品或项目与istio项目集成。

istio项目是service mesh概念的最新实现,旨在所有主流集群管理平台上提供service mesh层,初期以实现Kubernetes上的服务治理层为目标。它由控制平面和数据平面组成(是不是感觉和SDN的设计理念相似啊)。控制平面由Go语言实现,包括pilot、mixer、auth三个组件;数据平面功能暂由envoy在pod中以sidecar的部署形式提供。下面是官方的架构图:

img{512x368}
图:istio架构图(来自官网)

sidecar中envoy代理了pod中真正业务container的所有进出流量,并对这些流量按照控制平面设定的“治理逻辑”进行处理。而这一切对pod中的业务应用是透明的,开发人员可以专心于业务逻辑,而无需再关心微服务治理的逻辑。istio代表的service mesh的设计理念被认为是下一代“微服务统一框架”,甚至有人认为是微服务框架演化的终点。

istio于2017 年 5 月 24 日发布了0.1 release 版本,截至目前为止istio的版本更新到v0.4.0,演进速度相当快,不过目前依然不要用于生产环境,至少要等到1.0版本发布吧。但对于istio的早期接纳者而言,现在正是深入研究istio的好时机。在本篇的接下来内容中,我们将带领大家感性的认识一下istio,入个门儿。

三、istio安装

istio目前支持最好的就是kubernetes了,因此我们的实验环境就定在kubernetes上。至于版本,istio当前最新版本为0.4.0,这个版本据说要k8s 1.7.4及以上版本用起来才不会发生小毛病:)。我的k8s集群是v1.7.6版本的,恰好满足条件。下面是安装过程:(Node上的os是ubuntu 16.04)

# wget -c https://github.com/istio/istio/releases/download/0.4.0/istio-0.4.0-linux.tar.gz

解压后,进入istio-0.4.0目录,

# ls -F
bin/  install/  istio.VERSION  LICENSE  README.md  samples/

# cat istio.VERSION
# DO NOT EDIT THIS FILE MANUALLY instead use
# install/updateVersion.sh (see install/README.md)
export CA_HUB="docker.io/istio"
export CA_TAG="0.4.0"
export MIXER_HUB="docker.io/istio"
export MIXER_TAG="0.4.0"
export PILOT_HUB="docker.io/istio"
export PILOT_TAG="0.4.0"
export ISTIOCTL_URL="https://storage.googleapis.com/istio-release/releases/0.4.0/istioctl"
export PROXY_TAG="0.4.0"
export ISTIO_NAMESPACE="istio-system"
export AUTH_DEBIAN_URL="https://storage.googleapis.com/istio-release/releases/0.4.0/deb"
export PILOT_DEBIAN_URL="https://storage.googleapis.com/istio-release/releases/0.4.0/deb"
export PROXY_DEBIAN_URL="https://storage.googleapis.com/istio-release/releases/0.4.0/deb"
export FORTIO_HUB="docker.io/istio"
export FORTIO_TAG="0.4.2"

# cd install/kubernetes

我们先不用auth功能,因此使用istio.yaml这个文件进行istio组件安装:

# kubectl apply -f istio.yaml
namespace "istio-system" created
clusterrole "istio-pilot-istio-system" created
clusterrole "istio-initializer-istio-system" created
clusterrole "istio-mixer-istio-system" created
clusterrole "istio-ca-istio-system" created
clusterrole "istio-sidecar-istio-system" created
clusterrolebinding "istio-pilot-admin-role-binding-istio-system" created
clusterrolebinding "istio-initializer-admin-role-binding-istio-system" created
clusterrolebinding "istio-ca-role-binding-istio-system" created
clusterrolebinding "istio-ingress-admin-role-binding-istio-system" created
clusterrolebinding "istio-sidecar-role-binding-istio-system" created
clusterrolebinding "istio-mixer-admin-role-binding-istio-system" created
configmap "istio-mixer" created
service "istio-mixer" created
serviceaccount "istio-mixer-service-account" created
deployment "istio-mixer" created
customresourcedefinition "rules.config.istio.io" created
customresourcedefinition "attributemanifests.config.istio.io" created
... ...
customresourcedefinition "reportnothings.config.istio.io" created
attributemanifest "istioproxy" created
attributemanifest "kubernetes" created
stdio "handler" created
logentry "accesslog" created
rule "stdio" created
metric "requestcount" created
metric "requestduration" created
metric "requestsize" created
metric "responsesize" created
metric "tcpbytesent" created
metric "tcpbytereceived" created
prometheus "handler" created
rule "promhttp" created
rule "promtcp" created
kubernetesenv "handler" created
rule "kubeattrgenrulerule" created
kubernetes "attributes" created
configmap "istio" created
customresourcedefinition "destinationpolicies.config.istio.io" created
customresourcedefinition "egressrules.config.istio.io" created
customresourcedefinition "routerules.config.istio.io" created
service "istio-pilot" created
serviceaccount "istio-pilot-service-account" created
deployment "istio-pilot" created
service "istio-ingress" created
serviceaccount "istio-ingress-service-account" created
deployment "istio-ingress" created
serviceaccount "istio-ca-service-account" created
deployment "istio-ca" created

注:我还曾在k8s v1.7.3上安装过istio 0.3.0版本,但在创建组件时会报下面错误(这个错误可能会导致后续addon安装后工作不正常):

unable to recognize "istio.yaml": no matches for config.istio.io/, Kind=metric
unable to recognize "istio.yaml": no matches for config.istio.io/, Kind=metric
unable to recognize "istio.yaml": no matches for config.istio.io/, Kind=metric
unable to recognize "istio.yaml": no matches for config.istio.io/, Kind=metric
unable to recognize "istio.yaml": no matches for config.istio.io/, Kind=metric
unable to recognize "istio.yaml": no matches for config.istio.io/, Kind=metric

安装后,我们在istio-system这个namespace下会看到如下pod和service在运行(由于istio的各个组件的image size都不小,因此pod状态变为running需要一丢丢时间,耐心等待):

# kubectl get pods -n istio-system
NAME                             READY     STATUS    RESTARTS   AGE
istio-ca-1363003450-jskp5        1/1       Running   0          3d
istio-ingress-1005666339-c7776   1/1       Running   4          3d
istio-mixer-465004155-twhxq      3/3       Running   24         3d
istio-pilot-1861292947-6v37w     2/2       Running   18         3d

# kubectl get svc -n istio-system
NAME            CLUSTER-IP       EXTERNAL-IP   PORT(S)                                                   AGE
istio-ingress   10.98.10.87      <pending>     80:31759/TCP,443:25804/TCP                         4d
istio-mixer     10.109.244.155   <none>        9091/TCP,15004/TCP,9093/TCP,9094/TCP,9102/TCP,9125/UDP,42422/TCP   4d
istio-pilot     10.105.80.55     <none>        15003/TCP,443/TCP                                              4d

istio安装成功!

四、服务治理策略验证

接下来我们来用几个例子验证一下istio在服务治理方面的能力!(istio自带一些完整的例子,比如bookinfo,用于验证服务治理的能力,但这里先不打算用这些例子)

1、验证环境和拓扑

我们先来看一下验证环境的示意图:
img{512x368}

我们看到在service mesh中部署了两个service: server_a和service_b,前者调用后者完成某项业务,后者则调用外部服务完成业务逻辑。

  • service_a: 模拟pay服务,在收到client请求后,进行pay处理,并将处理结果通过service_b提供的msg notify服务下发给user。该服务的endpoint为/pay;
  • service_b: 模拟notify服务,在收到service_a请求后,将message转发给external service,完成notify逻辑。该服务的endpoint为/notify;
  • external service: 位于service mesh之外。
  • client:我们使用curl模拟。

img{512x368}

我们先来部署service_a和service_b的v0.1版本:

以service_a的部署为例, service_a的deployment文件如下:

//svca-v0.1.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: svca
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: svca
        version: v0.1
    spec:
      containers:
      - name: svca
        image: docker.io/bigwhite/istio-demo-svca:v0.1
        imagePullPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
  name: svca
  labels:
    app: svca
spec:
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP
  selector:
    app: svca

注意,我们部署service_a时不能直接使用kubectl apply -f svca-v0.1.yaml,而是要apply经过istioctl(需将istio安装目录下的bin放入PATH)处理过的yaml,以注入sidecar容器。当然也可以配置为自动为每个k8s启动的pod注入sidecar,但我们这里没有使用自动注入。我们执行下面命令:

# kubectl apply -f <(istioctl kube-inject -f svca-v0.1.yaml)
deployment "svca" created
service "svca" created

# kubectl get pods
NAME                               READY     STATUS    RESTARTS   AGE
svca-1997590752-tpwjf              2/2       Running   0          2m

同样的方法,我们来创建svcb:v0.1:

# kubectl apply -f <(istioctl kube-inject -f svcb-v0.1.yaml)
deployment "svcb" created
service "svcb" created

我们看到istio向每个pod中插入一个sidecar container,这个就是前面说的envoy,只不过container名字为istio-proxy。

接下来,我们把那个external service启动起来:

# nohup ./msgd > 1.log & 2>&1
[1] 9423

实验环境ok了。下面我们来验证一下业务是否是通的。

2、egress rule

按照之前我们的设定,我们使用curl去访问service_a服务的/pay端点,我们查看一下svca服务的ip和端口:

# kubectl get svc
NAME               CLUSTER-IP       EXTERNAL-IP   PORT(S)
svca               10.105.38.238    <none>        80/TCP                                         9h
svcb               10.105.119.194   <none>        80/TCP                                         9h

我们访问一下svca服务,svca的服务地址可以通过kubectl get svc查到:

# curl {svca_ip}/pay

查看svca和svcb的日志:

//service_a的日志:

service_a:v0.1 is serving the request...
service_a:v0.1 pays ok
&{500 Internal Server Error 500 HTTP/1.1 1 1 map[X-Content-Type-Options:[nosniff] Date:[Tue, 02 Jan 2018 15:41:50 GMT] Content-Length:[66] Content-Type:[text/plain; charset=utf-8]] 0xc420058d40 66 [] false false map[] 0xc4200eaf00 <nil>}
service_a:v0.1 notify customer ok

// service_b的日志:
&{GET /notify?msg=service_a:v0.1-pays-ok HTTP/1.1 1 1 map[User-Agent:[Go-http-client/1.1] Accept-Encoding:[gzip]] {} <nil> 0 [] false svcb map[] map[] <nil> map[] 127.0.0.1:58778 /notify?msg=service_a:v0.1-pays-ok <nil> <nil> <nil> 0xc4200fa3c0}
service_b:v0.1 is serving the request...
service_b:v0.1 send msg error: Get http://10.100.35.27:9997/send?msg=service_a:v0.1-pays-ok: EOF

我们看到service_a和service_b都返回了错误日志(注意:go http get方法对于non-2xx response不会返回错误,我们只是看到了response中的500状态码才意识到错误的存在)。其中源头在service_b,原因是其连不上那个external service!那么为什么连不上external service呢?这是由于缺省情况下,启用了Istio的服务是无法访问外部URL的,这是因为Pod中的iptables把所有外发传输都转向到了Sidecar代理,而这一代理只处理集群内的访问目标。因此位于service mesh内的服务svcb无法访问外部的服务(msgd),我们需要显式的添加egressrule规则:

我们创建一个允许svcb访问外部特定服务的EgressRule:

//rules/enable-svcb-engress-rule.yaml

apiVersion: config.istio.io/v1alpha2
kind: EgressRule
metadata:
  name: enable-svcb-engress-rule
spec:
  destination:
    service: 10.100.35.27
  ports:
    - port: 9997
      protocol: http

使规则生效:

# istioctl create -f enable-svcb-engress-rule.yaml
Created config egress-rule/default/enable-svcb-engress-rule at revision 30031258

这时你再尝试curl svca,我们可以看到msgd的日志中出现了下面的内容:

2018/01/02 23:58:16 &{GET /send?msg=service_a:v0.1-pays-ok HTTP/1.1 1 1 map[X-Ot-Span-Context:[2157e7ffb8105330;2157e7ffb8105330;0000000000000000] Content-Length:[0] User-Agent:[Go-http-client/1.1] X-Forwarded-Proto:[http] X-Request-Id:[13c3af6e-2f52-993d-905f-aa6aa4b57e2d] X-Envoy-Decorator-Operation:[default-route] X-B3-Spanid:[2157e7ffb8105330] X-B3-Sampled:[1] Accept-Encoding:[gzip] X-B3-Traceid:[2157e7ffb8105330] X-Istio-Attributes:[Ch8KCXNvdXJjZS5pcBISMhAAAAAAAAAAAAAA//8KLgAMCjoKCnNvdXJjZS51aWQSLBIqa3ViZXJuZXRlczovL3N2Y2ItMjAwODk3Mzc2OS1ncTBsaC5kZWZhdWx0]] {} <nil> 0 [] false 10.100.35.27:9997 map[] map[] <nil> map[] 10.100.35.28:38188 /send?msg=service_a:v0.1-pays-ok <nil> <nil> <nil> 0xc4200584c0}
2018/01/02 23:58:16 Msgd is serving the request...
2018/01/02 23:58:16 Msgd recv msg ok, msg= service_a:v0.1-pays-ok

说明Svcb到外部服务的通信被打通了!

3、迁移流量到新版本svcb:v0.2

我们经常有这样的需求,当svcb运行一段时间后,svcb添加了新feature,版本要升级到v0.2了,这时我们会部署svcb:v0.2,并将流量逐步切到v0.2上。

我们先来部署一下svcb:v0.2:

// svcb-v0.2.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: svcb-v0.2
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: svcb
        version: v0.2
    spec:
      containers:
      - name: svcb
        image: docker.io/bigwhite/istio-demo-svcb:v0.2
        imagePullPolicy: Always

我们可以看到,服务名不变,但版本的label变成了v0.2,我们来执行这次部署:

# kubectl apply -f <(istioctl kube-inject -f svcb-v0.2.yaml)
deployment "svcb-v0.2" created

# kubectl get pods
NAME                               READY     STATUS    RESTARTS   AGE
svca-1997590752-pq9zg              2/2       Running   0          9h
svcb-2008973769-gq0lh              2/2       Running   0          9h
svcb-v0.2-3233505404-0g55w         2/2       Running   0          1m

svcb服务下又增加了一个endpoint:

# kubectl describe svc/svcb

.... ...
Selector:        app=svcb
Type:            ClusterIP
IP:            10.105.119.194
Port:            <unset>    80/TCP
Endpoints:        10.40.0.28:8080,10.46.0.12:8080
... ...

此时,如果按照k8s的调度方式,v0.1和v0.2版本的两个svcb pod应该1:1均衡地承载流量。为了方便查看流量分布,我们将每个版本的svcb的pod副本数量都扩展为2个(replicas: 2),这样service mesh中一共会有4个 svcb endpoints。

通过curl访问svca注入流量后,我们发现流量都集中在一个svcb:v0.2的pod上,并且长时间没有变化。我们通过下面的route rule规则来尝试将流量在svcb:v0.1和svcb:v0.2之间1:1均衡:

// route-rules-svcb-v0.2-50.yaml
apiVersion: config.istio.io/v1alpha2
kind: RouteRule
metadata:
  name: route-rules-svcb
spec:
  destination:
    name: svcb
  precedence: 1
  route:
  - labels:
      version: v0.1
    weight: 50
  - labels:
      version: v0.2
    weight: 50

# istioctl create -f route-rules-svcb-v0.2-50.yaml
Created config route-rule/default/route-rules-svcb at revision 30080638

按照istio文档中的说法,这个规则的生效需要一些时间。之后我们注入流量,发现流量切换到svcb:v0.1的一个pod上去了,并且很长一段时间不曾变化,未均衡到svcb:v0.2上去。

我们更新一下route rule,将流量全部切到svcb:v0.2上去:

//route-rules-svcb-v0.2-100.yaml
apiVersion: config.istio.io/v1alpha2
kind: RouteRule
metadata:
  name: route-rules-svcb
spec:
  destination:
    name: svcb
  precedence: 1
  route:
  - labels:
      version: v0.2
    weight: 100

# istioctl replace -f route-rules-svcb-v0.2-100.yaml
Updated config route-rule/default/route-rules-svcb to revision 30082944

我们用istio的replace命令更新了规则:route-rules-svcb。更新后,再次注入流量,这回流量重新集中在svcb:v0.2的一个pod上了,再过一段时间另外一个svcb:v0.2的pod上才有了一些流量。但svcb:v0.1上不再有流量,这个切换是成功的。

在k8s的service的负载均衡中,k8s就利用了iptables的概率转发(random –probability 0.5),因此这种流量均衡并非是精确的,只有在长时间大量流量经过后,才能看到流量的分布与设定的权重是相似的,可能istio也是如此,这里仅是入门,就不深入挖掘了。

当然istio在路由规则设施方面的“能耐”远不止上面例子中所展示的那样,如果要悉数列出,那本文的长度可是要爆掉了。有兴趣的朋友可以去翻看官方文档

五、插件安装

istio的强大微服务治理能力还体现在其集成了grafanaprometheus、servicegraph、zipkin等addons,应用程序无需做任何改动,就可以具有数据收集、度量与可视化的监控能力、服务的分布式跟踪能力等。我们可以在istio的安装包中找到这些addons的安装文件,我们来逐一试试。

1、prometheus & grafana

我们先来安装一下prometheus 和 grafana插件(位于istio-0.4.0/install/kubernetes/addon下面):

# kubectl apply -f prometheus.yaml
configmap "prometheus" created
service "prometheus" created
deployment "prometheus" created

# kubectl apply -f grafana.yaml
service "grafana" created
deployment "grafana" created

# kubectl get pods -n istio-system
NAME                             READY     STATUS    RESTARTS   AGE
grafana-3617079618-zpglx         1/1       Running   0          5m
prometheus-168775884-ppfxr       1/1       Running   0          5m
... ...

# kubectl get svc -n istio-system
NAME            CLUSTER-IP       EXTERNAL-IP   PORT(S)            AGE
grafana         10.105.21.25     <none>        3000/TCP                     16m
prometheus      10.103.160.37    <none>        9090/TCP                16m
... ...

浏览器中输入prometheus的服务地址http://10.103.160.37:9090,访问prometheus:

img{512x368}

点击菜单项:status -> targets,查看各个target的状态是否正常:

img{512x368}

如果像上图所示那样,各个target都是up状态,那就说明istio运行时ok的。否则请参考istio troubleshooting中的内容对istio逐一进行排查,尤其是istio-mesh这个Target在istio-0.3.0+kubernetes 1.7.3的环境中就是Down的状态。

浏览器输入grafana的服务地址:http://10.105.21.25:3000/,打开grafana面板:

img{512x368}

切换到Istio Dashboard,并向istio service mesh注入流量,我们会看到仪表盘变化如下:

img{512x368}

img{512x368}

2、servicegraph

servicegraph插件是用来查看服务调用关系的,我们来创建一下该组件:

# kubectl apply -f servicegraph.yaml
deployment "servicegraph" created
service "servicegraph" created

# kubectl get svc -n istio-system
NAME            CLUSTER-IP       EXTERNAL-IP   PORT(S)                 AGE
servicegraph    10.108.245.21    <none>        8088/TCP                     52s
... ...

创建成功后,向service mesh网络注入流量,然后访问servicegraph:http://{servicegraph_ip}:8088/dotviz,在我的环境里,我看到的图示如下:

img{512x368}

调用关系似乎有些乱,难道是我在程序使用的调用方法不够标准?:(

3、zipkin

istio集成了zipkin,利用zipkin我们可以做分布式服务调用的追踪。之前自己曾经搭建过基于jaegeropentracing的分布式调用服务,十分繁琐。并且要想使用tracing,对应用代码的侵入必不可少。

我们安装一下zipkin addon:

# kubectl apply -f zipkin.yaml
deployment "zipkin" created
service "zipkin" created

# kubectl get svc -n istio-system
NAME            CLUSTER-IP       EXTERNAL-IP   PORT(S)                  AGE
zipkin          10.105.7.219     <none>        9411/TCP                             1h

我们访问以下zikpin的UI,通过浏览器打开http://{zipkin_service_ip}:9411。

img{512x368}

接下来,我们向service mesh注入一些流量,然后再zipkin首页的“服务名”下拉框中选择”svcb”,查找跟踪情况:

img{512x368}

我们看到:在没有对svca, svcb做任何修改的情况下,我们依然可以在zipkin中找到svcb相关的调用。点击其中一个trace,可以查看细节:

img{512x368}

当然如果你想做内容更为丰富的、更为强大的跟踪,可能需要在应用代码中做些配合,具体可以参见:istio的分布式跟踪

六、小结

istio项目诞生不到一年,目前离成熟还远。快速积极开发可能会导致istio的接口和实现机制都会发生很大的变化,因此本文不能保证内容将适用于后续所有istio的发布版本

本文涉及到的源码在这里可以下载到,demo service的镜像可以在我的docker hub上pull


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

微信赞赏:
img{512x368}

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!




这里是Tony Bai的个人Blog,欢迎访问、订阅和留言!订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:


如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:


以太币:


如果您喜欢通过微信App浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:



本站Powered by Digital Ocean VPS。

选择Digital Ocean VPS主机,即可获得10美元现金充值,可免费使用两个月哟!

著名主机提供商Linode 10$优惠码:linode10,在这里注册即可免费获得。

阿里云推荐码:1WFZ0V立享9折!

View Tony Bai's profile on LinkedIn


文章

评论

  • 正在加载...

分类

标签

归档











更多