Fork me on GitHub

2023年4月

使用 Google Colab 体验 AI 绘画

AIGC 的全称为 AI Generated Content,是指利用人工智能技术来生成内容,被认为是继 PGC(Professionally Generated Content,专业生成内容)和 UGC(User Generated Content,用户生成内容)之后的一种新型内容创作方式。目前,这种创作方式一般可分为两大派别:一个是以 OpenAIChatGPTGPT-4、Facebook 的 LLaMA、斯坦福的 Alpaca大语言模型 技术为代表的文本生成派,另一个是以 Stability AIStable DiffusionMidjourney、OpenAI 的 DALL·E 2扩散模型 技术为代表的图片生成派。

在文本生成方面,目前 AI 已经可以和用户聊天,回答各种问题,而且可以基于用户的要求进行文本创作,比如写文案、写邮件、写小说等;在图片生成方面,AI 的绘画水平也突飞猛进,目前 AI 已经可以根据用户的提示词创作出各种不同风格的绘画作品,而且可以对图片进行风格迁移、自动上色、缺损修复等,AI 生成的作品几乎可以媲美专业画师,生成作品的效率越来越高,而生成作品的成本却越来越低,这让 AI 绘画技术得以迅速普及,让普通用户也可以体验专业画师的感觉,我从小就很特别羡慕那些会画画的人,现在就可以借助 AI 技术让我实现一个画家的梦。

AI 绘画的发展历史

2014 年 10 月,Ian J. Goodfellow 等人发表了一篇论文 《Generative Adversarial Networks》,在论文中提出了一种新的深度学习算法 GAN(生成式对抗网络),这个算法包含两个模型:生成模型(Generative Model,简称 G 模型)和 判别模型(Discriminative Model,简称 D 模型),在训练过程中,G 模型的目标是尽量生成以假乱真的图片去欺骗 D 模型,而 D 模型的目标是判断 G 模型生成的图片是不是真实的,这样,G 模型和 D 模型就构成了一个动态的博弈过程,仿佛老顽童周伯通的左右手互搏一样,当 D 模型无法判断输入的图片是 G 模型生成的还是真实的时候,G 模型和 D 模型的训练就达到了平衡,这时我们得到的 G 模型就可以生成以假乱真的图片了。

不过由于 GAN 算法包含了两个模型,稳定性较差,可能出现有趣的 海奥维提卡现象(the helvetica scenario),如果 G 模型发现了一个能够骗过 D 模型的 bug,它就会开始偷懒,一直用这张图片来欺骗 D 模型,导致整个平衡的无效。在 2020 年,Jonathan Ho 等人发表论文 《Denoising Diffusion Probabilistic Models》,提出了一种新的 扩散模型(Diffusion Model),相比 GAN 来说,扩散模型的训练更稳定,而且能够生成更多样的样本,一时间扩散模型在 AI 圈里迅速起飞,2021 年 11 月 OpenAI 推出 DALL·E,2022 年 3 月,David Holz 推出 Midjourney,5 月 Google Brain 推出 Imagen,都是基于扩散模型实现的。

到了 2022 年 8 月,Stability AI 开发出 Stable Diffusion 模型,相比于之前的商业产品,Stable Diffusion 是一个完全开源的模型,无论是代码还是权重参数库都对所有人开放使用,而且 Stable Diffusion 对资源的消耗大幅降低,消费级显卡就可以驱动,大大降低了 AI 绘画的门槛,普通人也可以在他们的电脑上体验 AI 绘画的乐趣。到了 10 月,游戏设计师 Jason Allen 使用 AI 绘画工具 Midjourney 生成的一幅名为《太空歌剧院》的作品在美国科罗拉多州举办的艺术博览会上获得数字艺术类冠军,引起了一波不小的争论,也让 AI 绘画再一次成为热门话题,之后各大公司和团队纷纷入局,各种 AI 绘画工具如雨后春笋般冒了出来。

正因为如此,有人将 2022 年称为 AI 绘画元年。

选择 GPU

虽说 Stable Diffusion 的门槛已经被大大降低了,但还是有一定门槛的,因为运行 Stable Diffusion 要配备一张 GPU 显卡,可以使用 NVIDIA 卡(俗称 N 卡)或 AMD 卡(俗称 A 卡),不过主流的推理框架都使用了 NVIDIA 的 CUDA 工具包,所以一般都推荐使用 N 卡。GPU 显卡价格不菲,可以参考驱动之家的 桌面显卡性能天梯图 进行选购,除非你是资深的游戏玩家或者深度学习的爱好者,大多数家用电脑上都不具备这个条件。

也可以使用各大公有云厂商推出的 GPU 云服务器,比如 阿里云腾讯云华为云百度智能云 等,但是价格也都不便宜,比较适合中小企业,对于那些刚对深度学习感兴趣,希望尝试一些深度学习项目的小白个人用户来说,就不划算了。

好在网上有很多 白嫖 GPU 的攻略,国外的有 Google Colab 和 Kaggle,它们都提供了 V100、P100、T4 等主流显卡,可以免费使用 12 个小时,超时之后会自动清理;国内的有阿里的天池,相比来说磁盘和使用时间稍短一点,不过对于新人入门来说也足够了;另外还有百度的 AI Studio 和 趋动云 等产品,它们可以通过打卡做任务等形式赚取 GPU 算力,在 GPU 不够用时不妨一试。下面是网上找的一些使用教程,供参考:

Google Colab 入门

综合对比下来,Google Colab 的使用体验最好,Google Colab 又叫作 Colaboratory,简称 Colab,中文意思是 合作实验室,正如其名,它可以帮助用户在浏览器中编写和执行 Python 代码,无需任何配置就可以进行一些数据科学或机器学习的实验,借助 Jupyter 交互式笔记本,实验过程和结果也可以轻松分享给其他用户。很多开源项目都提供了 Colab 脚本,可以直接运行体验,这一节将以 Colab 为例,介绍它的基本使用方法。

首先在浏览器输入 colab.research.google.com 访问 Colab 首页:

colab-home.png

首页上对 Colab 有个简单的介绍,还提供了一些数据科学和机器学习的入门例子和学习资源。我们通过左上角的 文件 -> 新建笔记本 菜单项创建一个新的笔记本:

new-notebook.png

然后点击 修改 -> 笔记本设置 将硬件加速器调整为 GPU:

notebook-setting.png

然后点击右上角的 连接 按钮,Google 会动态地为我们分配计算资源,稍等片刻,我们就相当于拥有了一台 12.7 G 内存,78.2 G 磁盘,且带 GPU 显卡的主机了:

resource-info.png

Colab 的基本使用

在这个笔记本中,我们可以编写 Markdown 文档,也可以编写和执行 Python 代码:

execute-python-code.png

甚至可以在命令前加个 ! 来执行 Shell 命令:

execute-shell.png

这个环境里内置了很多常用的数据科学或机器学习的 Python 库,比如 numpy、pandas、matplotlib、scikit-learn、tensorflow 等:

pip-list.png

另外,由于这是一台 GPU 主机,我们还可以使用 nvidia-smi 来查看显卡信息:

nvidia-smi.png

可以看到,我们免费得到了一张 Tesla T4 的显卡,显存大约 15G 左右。

测试 GPU 速度

接下来,我们测试下这个 GPU 的速度。首先通过 TensorFlow 的 tf.test.gpu_device_name() 获取 GPU 设备的名称:

gpu-device-name.png

然后编写两个方法:

gpu-vs-cpu.png

这两个方法所做的事情是一样的,只不过一个使用 CPU 来运行,另一个使用 GPU 来运行。在这个方法中,先使用 tf.random.normal((100, 100, 100, 3)) 随机生成一个 100*100*100*3 的四维张量,然后使用 tf.keras.layers.Conv2D(32, 7)(random_image_cpu) 对这个张量计算卷积,卷积过滤器数量 filters 为 32,卷积窗口 kernel_size7*7,最后使用 tf.math.reduce_sum(net_cpu) 对卷积结果求和。

接下来第一次执行,并使用 timeit 来计时:

gpu-vs-cpu-first-run.png

可以看到,在 GPU 上的执行速度比 CPU 上的要慢一点,这是因为 TensorFlow 第一次运行时默认会使用 cuDNN 的 autotune 机制对计算进行预热。

我们再执行第二次:

gpu-vs-cpu-second-run.png

这时,在 GPU 上的执行速度明显快多了,相比于 CPU 来说,速度有着 50 多倍的提升。

这里是 这一节的完整代码

在 Google Colab 里运行 Stable Diffusion

2023 年 4 月 21 日,Google Colab 官方发了一份声明,由于 Stable Diffusion 太火了,消耗了 Google Colab 大量的 GPU 资源,导致预算不够,现在已经被封了,只有付费用户才能运行,免费用户运行会有警告:

colab-warning.png

对 Google Colab 有一定了解后,我们就可以免费使用它的 GPU 来做很多有趣的事情了,比如我想要运行 Stable Diffusion 来体验 AI 绘画。

camenduru/stable-diffusion-webui-colab 这个项目整理了大量 Stable Diffusion 的 Colab 脚本,基于 AUTOMATIC1111/stable-diffusion-webui 实现了可视化 Web 页面,集成了 Hugging FaceCivitai 上热门的模型和插件,我们随便选择一个,点击左侧的 stable 打开 Colab 页面执行即可:

sd-webui-colab.png

运行成功后,在控制台中可以看到打印了几个随机生成的外网链接:

running.png

随便选择一个链接打开,进入 Stable Diffusion WebUI 页面:

webui.png

接下来,开始你的 AI 绘画之旅吧!

参考

更多

文本生成派

图片生成派

扫描二维码,在手机上阅读!

使用 RSSHub 为任意网址生成订阅源

最近在学习 APISIX 时,发现它的 官方博客 有不少的干货内容,于是想着能在我的阅读器里订阅这个博客的更新,不过找了半天都没有找到这个博客的订阅入口,后来在博客的页面代码里找到了 rss.xml 和 atom.xml 两个订阅链接,不过打开一看全都是 404 Page Not Found

其实遇到这种情况,也有不少的解决方法,有很多网站提供了 RSS 生成的功能,比如 RSS.appFetchRSSfeed43 等都提供了免费的 RSS 源转换功能,不过这些工具要么使用起来不太好用,要么访问速度巨慢,要么就是有各种各样的限制。于是便想实现一个自己的 RSS 生成服务,正好前几天看到了一个叫做 RSSHub 的项目,这是一个开源、简单易用、易于扩展的 RSS 生成器,口号是 万物皆可 RSS,可以给任何奇奇怪怪的内容生成 RSS 订阅源,而且看社区也挺活跃,于是就利用周末时间折腾一下,使用 RSSHub 搭建了一个自己的 RSS 生成服务。

快速开始

RSSHub 和那些在线的 RSS 生成服务不一样,它是通过编写扩展的方式来添加新的 RSS 订阅源。不过在编写自己的扩展之前,可以先到官网上搜索一下,看看有没有其他人已经写过了,官网上目前已经适配了数百家网站的上千项内容。由于我要订阅的 APSIX 博客比较小众,目前还没有人写过,所以就只能自己动手了。

RSSHub 是基于 Node.js 实现的,所以先确保机器上已经安装了 Node.js 运行环境:

$ node -v
v16.14.2

以及包管理器 Npm 或 Yarn,我这里使用的是 Npm:

$ npm -v
8.5.0

然后,下载 RSSHub 的源码:

$ git clone https://github.com/DIYgod/RSSHub.git

进入 RSSHub 的根目录,运行以下命令安装依赖:

$ cd RSSHub
$ npm install

依赖安装成功后,运行以下命令在本地启动 RSSHub:

$ npm run dev

启动成功后,在浏览器中打开 http://localhost:1200 就可以看到 RSSHub 的首页了:

rsshub-homepage.png

新建路由

此时 RSSHub 内置的上千个订阅源,都可以在本地访问,比如通过 /bilibili/ranking/0/3/1 这个地址可以订阅 B 站三天内的排行榜。这个订阅源的格式一般分为三个部分:

/命名空间/路由/参数

新建命名空间

命名空间应该和 RSS 源网站的二级域名相同,所以 B 站的命名空间为 bilibili,而我们要新建的 APISIX 博客地址为 apisix.apache.org/zh/blog,所以命名空间应该为 apache

每个命名空间对应 lib/v2 目录下的一个子文件夹,所以我们在这个目录下创建一个 apache 子文件夹:

$ mkdir lib/v2/apache

注册路由

第二步,我们需要在命名空间子文件夹下按照 RSSHub 的 路由规范 来组织文件,一个典型的文件夹结构如下:

├───lib/v2
│   ├───furstar
│       ├─── templates
│           ├─── description.art
│       ├─── router.js
│       ├─── maintainer.js
│       ├─── radar.js
│       └─── someOtherJs.js

其中,每个文件的作用如下:

  • router.js - 注册路由
  • maintainer.js - 提供路由维护者信息
  • radar.js - 为每个路由提供对应 RSSHub Radar 规则
  • someOtherJs.js - 一些其他的代码文件,一般用于实现路由规则
  • templates - 该目录下是以 .art 结尾的模版文件,它使用 art-template 进行排版,用于渲染自定义 HTML 内容

编写 router.js 文件

其中最重要的一个文件是 router.js,它用于注册路由信息,我们创建该文件,内容如下:

module.exports = (router) => {
    router.get('/apisix/blog', require('./apisix/blog'));
};

RSSHub 使用 @koa/router 来定义路由,在上面的代码中,我们通过 router.get() 定义了一个 HTTP GET 的路由,第一个参数是路由路径,它需要符合 path-to-regexp 语法,第二个参数指定由哪个文件来实现路由规则。

在路由路径中,我们还可以使用参数,比如上面 bilibili 的路由如下:

router.get('/ranking/:rid?/:day?/:arc_type?/:disableEmbed?', require('./ranking'));

其中 :rid:days:arc_type:disableEmbed 都是路由的参数,每个参数后面的 ? 表示这是一个可选参数。路由参数可以从 ctx.params 对象中获取。

编写 maintainer.js 文件

maintainer.js 文件用于提供路由维护者信息,当用户遇到 RSS 路由的问题时,他们可以联系此文件中列出的维护者:

module.exports = {
    '/apisix/blog': ['aneasystone'],
};

编写路由规则

接下来我们就可以实现路由规则了。首先我们需要访问指定网址来获取数据,RSSHub 提供了两种方式来获取数据:

  • 对于一些简单的 API 接口或网页,可以直接使用 got 发送 HTTP 请求获取数据;
  • 对于某些反爬策略很严的网页,可能需要使用 puppeteer 模拟浏览器打开网页来获取数据。

这其实就是爬虫技术,我们获取的数据通常是 JSON 或 HTML 格式,如果是 HTML 格式,RSSHub 提供了 cheerio 方便我们进一步处理。

上面在注册路由时我们指定了路由规则文件为 ./apisix/blog,所以接下来,创建 ./apisix/blog.js 文件。路由规则实际上就是生成 ctx.state.data 对象,这个对象包含三个字段:

  • title - 源标题
  • link - 源链接
  • item - 源文章

我们先编写一个最简单的路由规则,文件内容如下:

module.exports = async (ctx) => {
    ctx.state.data = {
        title: `Blog | Apache APISIX`,
        link: `https://apisix.apache.org/zh/blog/`,
        item: [{}],
    };
};

这时虽然源文章列表还是空的,但是我们已经可以通过 http://localhost:1200/apache/apisix/blog 地址来访问我们创建的 RSS 源了:

apache-apisix-blog-rss.png

只不过源文章中的 titledescription 都是 undefined

接下来要做的事情就是如何获取源文章了,很显然,我们需要访问 APISIX 的博客,并从页面 HTML 中解析出源文章。首先使用 got 发送 HTTP 请求获取页面 HTML:

const url = 'https://apisix.apache.org/zh/blog/';
const { data: res } = await got(url);

得到 HTML 之后,使用 cheerio 对其进行解析:

const $ = cheerio.load(res);

cheerio 有点类似于 jQuery,可以通过 CSS 选择器对 HTML 进行解析和提取,我们可以很方便地在页面中提取出源文章列表:

const articles = $('section.sec_gjjg').eq(1).find('article');
const results = [];
articles.each((i, elem) => {
    const a = $(elem).find('header > a');
    const time = $(elem).find('footer').find('time').text();
    const author = $(elem).find('footer').find('img').attr('src');
    results.push({
        title: a.find('h2').text(),
        description: a.find('p').text(),
        link: a.attr('href'),
        pubDate: timezone(parseDate(time, 'YYYY年M月D日'), +8),
        author,
    });
});
return results;

每个源文章包含以下几个字段:

  • title - 文章标题
  • link - 文章链接
  • description - 文章正文
  • pubDate - 文章发布日期
  • author - 文章作者(可选)
  • category - 文章分类(可选)

至此,我们的路由规则就创建好了,可以在浏览器中对我们的路由进行验证和调试。我们这里的路由规则比较简单,稍微复杂一点的例子可以参考 RSSHub 官方文档 制作自己的 RSSHub 路由使用缓存日期处理。另外,lib/v2 目录下有很多其他人编写的路由规则,也是很好的参考资料。

其他工作

实现自己的订阅源之后,还可以编写 radar.js 文件,为每个路由提供对应 RSSHub Radar 规则。RSSHub Radar 是 RSSHub 的一款浏览器插件,方便用户查找某个网站是否存在 RSS 订阅源。最后为你的路由 添加相应的文档,一个订阅源就开发完成了。

不过如果只是自己部署使用,这些工作也可以跳过。

部署

最后,将 RSSHub 部署到自己的服务器上,官方提供了几种 部署方式,比较推荐的是 Docker 或 Docker Compose 部署。

我这里使用 Docker 来简化部署流程。由于我希望将 Redis 作为我的 RSSHub 缓存,这样可以保证每次重启 RSSHub 之后缓存不会失效。首先启动一个 Redis 实例:

$ docker run -d -p 6379:6379 redis:alpine

然后启动 RSSHub 即可:

$ docker run --name rsshub \
    -d -p 1200:1200 \
    -e NODE_ENV=production \
    -e CACHE_TYPE=redis \
    -e REDIS_URL=redis://172.18.0.1:6379/ \
    -v /root/rsshub/v2:/app/lib/v2 \
    diygod/rsshub

注意我们将 lib/v2 目录挂载进容器,这样才能让我们的订阅源生效。我制作了三个 RSS 订阅源,有需要的小伙伴可以自取:

参考

扫描二维码,在手机上阅读!

使用 Istio 和 Envoy 打造 Service Mesh 微服务架构

周志明 老师在他的 《凤凰架构》 中将分布式服务通信的演化历史分成五个阶段:

  • 第一阶段:将通信的非功能性需求视作业务需求的一部分,通信的可靠性由程序员来保障

stage-1.png

这个阶段是分布式系统发展最早期时的技术策略,这些系统一开始仅仅是通过 RPC 技术去访问远程服务,当遇到通信问题时,就在业务代码中引入相应的处理逻辑,比如服务发现、网络重试或降级等。这些通信的非功能性逻辑和业务逻辑耦合在一起,让系统变得越来越复杂。

  • 第二阶段:将代码中的通信功能抽离重构成公共组件库,通信的可靠性由专业的平台程序员来保障

stage-2.png

这个阶段人们逐渐意识到通信功能应该从业务逻辑中抽离出来,于是形成了一些公共组件库,比如 FinagleSpring Cloud 等。不过这些组件库大多是和语言绑定的,比如 Spring Cloud 技术栈只能用在 Java 项目中,遇到其他语言的项目就无能为力了。

  • 第三阶段:将负责通信的公共组件库分离到进程之外,程序间通过网络代理来交互,通信的可靠性由专门的网络代理提供商来保障

stage-3.png

为了将通信功能做成语言无关的,这个阶段发展出了专门负责可靠通信的网络代理,比如 Netflix Prana。业务系统和这类代理之间通过回环设备或 Unix 套接字进行通信,网络代理对业务系统的网络流量进行拦截,从而在代理上完成流控、重试或降级等几乎所有的分布式通信功能。

这种网络代理后来演化出了两种形态:一种是微服务网关,它位于整个分布式系统的入口,同时代理整个系统中所有服务的网络流量;另一种是边车代理,它和某个进程共享一个网络命名空间,专门针对性地代理该服务的网络流量。

  • 第四阶段:将网络代理以边车的形式注入到应用容器,自动劫持应用的网络流量,通信的可靠性由专门的通信基础设施来保障

stage-4.png

在使用上一阶段的网络代理时,我们必须在应用程序中手工指定网络代理的地址才能生效,而边车代理有一个很大的优势,它通过 iptables 等技术自动劫持代理进程的网络流量,所以它对应用是完全透明的,无需对应用程序做任何改动就可以增强应用的通信功能。目前边车代理的代表性产品有 LinkerdEnvoyMOSN 等。不过,边车代理也有一个很大的缺点,随着系统中代理服务的增多,对边车代理的维护和管理工作就成了运维最头疼的问题,于是服务网格应运而生。

  • 第五阶段:将边车代理统一管控起来实现安全、可控、可观测的通信,将数据平面与控制平面分离开来,实现通用、透明的通信,这项工作就由专门的服务网格框架来保障

服务网格(Service Mesh)一词是 Buoyant 公司的 CEO William Morgan 于 2017 年在他的一篇博客 《What's a service mesh? And why do I need one?》 中首次提出的,他是世界上第一款服务网格产品 Linkerd 的创始人之一,在博客中,William Morgan 对服务网格做了如下定义:

服务网格是一种用于管控服务间通信的的基础设施,它负责为现代云原生应用程序在复杂服务拓扑中可靠地传递请求。在实践中,服务网格通常以轻量级网络代理阵列的形式实现,这些代理与应用程序部署在一起,对应用程序来说无需感知代理的存在。

服务网格将上一阶段的边车代理联合起来形成如下所示的网格状结构:

stage-5.png

在服务网格中,主要由数据平面与控制平面组成。数据平面是所有边车代理的集合,负责拦截所有服务流入和流出的流量,并配置控制平面对流量进行管理;控制平面对数据平面进行管理,完成配置分发、服务发现、授权鉴权等功能。如上图所示,整个系统中的通信包括两个部分:实线表示数据平面之间的通信,虚线表示控制平面和数据平面之间的通信。

服务网格的概念一经提出,其价值迅速被业界所认可,业界几乎所有的云原生玩家都积极参与了进来:

  • 2016 年,Buoyant 公司 推出 Linkerd,同年,Lyft 公司 推出 Envoy
  • 2017 年,Linkerd 加入 CNCF,同年,Google、IBM 和 Lyft 共同发布 Istio,为了和 Istio 展开竞争,Buoyant 公司将自家的 Conduit 产品合并到 Linkerd 中发布了 Linkerd 2
  • 2018 年后,Google、亚马逊、微软分别推出各自的公有云版本 Service Mesh 产品,国内的阿里巴巴也推出了基于 Istio 的修改版 SOFAMesh(目前已经废弃),并开源了自己研发的 MOSN 代理

随着各巨头的参与,Istio 逐渐超过了 Linkerd 的地位,几乎成了原云生环境下服务网格中控制平面的事实标准,而 Envoy 凭借其卓越的性能和强大的动态配置功能,成为了服务网格中数据平面的不二选择,下图是使用 Istio 作为服务网格方案后的典型架构:

istio-envoy.png

Istio 服务网格的数据平面由 Envoy 代理实现,在 Envoy 学习笔记 中我们学习过 Envoy 的基本用法,这是一个用 C++ 开发的高性能代理,用于协调和控制微服务之间的网络流量,并为这些服务透明地提供了许多 Envoy 内置的功能特性:

  • 动态服务发现
  • 负载均衡
  • TLS 终端
  • HTTP/2 与 gRPC 代理
  • 熔断器
  • 健康检查
  • 基于百分比流量分割的分阶段发布
  • 故障注入
  • 丰富的指标

不仅如此,这些服务同时还具备了 Istio 所提供的功能特性:

  • 流量控制特性:通过丰富的 HTTP、gRPC、WebSocket 和 TCP 流量路由规则来执行细粒度的流量控制;
  • 网络弹性特性:重试设置、故障转移、熔断器和故障注入;
  • 安全性和身份认证特性:执行安全性策略,并强制实行通过配置 API 定义的访问控制和速率限制;
  • 基于 WebAssembly 的可插拔扩展模型,允许通过自定义策略执行和生成网格流量的遥测。

Istio 服务网格的控制平面由 Istiod 实现,它提供了诸如服务发现(Pilot),配置(Galley),证书生成(Citadel)和可扩展性(Mixer)等功能;它通过 Envoy API 实现了对数据平面的管理,所以 Istio 的数据平面并不仅限于 Envoy,其他符合 Envoy API 规范的代理都可以作为 Istio 的数据平面。

快速开始

这篇笔记将以 Istio 的官方示例来学习如何使用 Istio 和 Envoy 打造一个基于服务网格的微服务架构。

安装 Istio

首先从 Istio 的 Release 页面 找到最新版本的安装包并下载和解压:

$ curl -LO https://github.com/istio/istio/releases/download/1.17.1/istio-1.17.1-linux-amd64.tar.gz
$ tar zxvf istio-1.17.1-linux-amd64.tar.gz

解压后的目录中包含几个文件和目录:

$ tree -L 1 istio-1.17.1
istio-1.17.1
├── LICENSE
├── README.md
├── bin
├── manifest.yaml
├── manifests
├── samples
└── tools

4 directories, 3 files

其中,bin 目录下包含了 istioctl 可执行文件,我们可以将这个目录添加到 PATH 环境变量,配置之后,就可以使用 istioctl install 命令安装 Istio 了:

$ istioctl install --set profile=demo
This will install the Istio 1.17.1 demo profile with ["Istio core" "Istiod" "Ingress gateways" "Egress gateways"] components into the cluster. Proceed? (y/N) y
✔ Istio core installed
✔ Istiod installed
✔ Ingress gateways installed
✔ Egress gateways installed
✔ Installation complete

Istio 内置了几种 不同的安装配置

$ istioctl profile list
Istio configuration profiles:
    ambient
    default
    demo
    empty
    external
    minimal
    openshift
    preview
    remote

所有的配置都位于 manifests/profiles 目录下,你也可以 定制自己的配置。这里为了演示和体验,我们使用了 demo 配置,它相对于默认的 default 配置来说,开启了一些用于演示的特性,日志级别也比较高,性能会有一定影响,所以不推荐在生产环境使用。这些配置通过统一的 IstioOperator API 来定义,我们可以通过 istioctl profile dump 查看配置详情:

$ istioctl profile dump demo

或者通过 istioctl profile diff 对比两个配置之间的差异:

$ istioctl profile diff default demo

执行 istioctl 命令时会创建了一个名为 installed-state 的自定义资源,内容就是上面所看到的 demo 配置:

$ kubectl get IstioOperator installed-state -n istio-system -o yaml

从安装的输出结果可以看到,demo 配置安装内容包括下面四个部分:

  • Istio core
  • Istiod
  • Ingress gateways
  • Egress gateways

可以使用 kubectl get deployments 来看看 Istio 具体安装了哪些服务:

$ kubectl get deployments -n istio-system
NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
istio-egressgateway    1/1     1            1           20m
istio-ingressgateway   1/1     1            1           20m
istiod                 1/1     1            1           21m

除此之外,istioctl 命令还生成了很多其他的 Kubernetes 资源,我们可以使用 istioctl manifest generate 生成最原始的 Kubernetes YAML 定义:

$ istioctl manifest generate --set profile=demo > demo.yaml

生成上面的 YAML 后,我们甚至可以通过执行 kubectl apply -f 来安装 Istio,不过这种安装方式有很多 需要注意的地方,官方并不推荐这种做法,但这也不失为一种深入了解 Istio 原理的好方法。

除了 使用 Istioctl 安装,官方还提供了很多 其他的安装方式,比如 使用 Helm 安装虚拟机安装使用 Istio Operator 安装 等,各个云平台 也对 Istio 提供了支持。

如果要卸载 Istio,可以执行 istioctl uninstall 命令:

$ istioctl uninstall -y --purge

一个简单的例子

为了充分发挥 Istio 的所有特性,网格中的每个应用服务都必须有一个边车代理,这个边车代理可以拦截应用服务的所有出入流量,这样我们就可以利用 Istio 控制平面为应用提供服务发现、限流、可观测性等功能了。

边车代理的注入 一般有两种方法:自动注入和手动注入。上面在解压 Istio 安装包时,可以看到有一个 samples 目录,这个目录里包含了一些官方的示例程序,用于体验 Istio 的不同功能特性,其中 samples/sleep 目录下是一个简单的 sleep 应用,这一节就使用这个简单的例子来演示如何在我们的应用服务中注入一个边车代理。

首先,执行下面的命令部署 sleep 应用:

$ kubectl apply -f samples/sleep/sleep.yaml
serviceaccount/sleep created
service/sleep created
deployment.apps/sleep created

默认情况下,一个 Pod 里只有一个容器(从下面的 READY 字段是 1/1 可以看出来):

$ kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
sleep-78ff5975c6-hw2lv   1/1     Running   0          23s

接下来,我们给 default 命名空间打上 istio-injection=enabled 标签:

$ kubectl label namespace default istio-injection=enabled
namespace/default labeled

这个标签可以让 Istio 部署应用时自动注入 Envoy 边车代理,这个过程是在 Pod 创建时自动完成的,Istio 使用了 Kubernetes 的 准入控制器(Admission Controllers),通过 MutatingAdmissionWebhook 可以对创建的 Pod 进行修改,从而将边车代理容器注入到原始的 Pod 定义中。

我们将刚刚的 Pod 删除,这样 Deployment 会尝试创建一个新的 Pod,从而触发自动注入的过程:

$ kubectl delete pod sleep-78ff5975c6-hw2lv
pod "sleep-78ff5975c6-hw2lv" deleted

再次查看 Pod 列表,READY 字段已经变成了 2/2,说明每个 Pod 里变成了两个容器:

$ kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
sleep-78ff5975c6-v6qg4   2/2     Running   0          12s

也可以使用 kubectl describe 查看 Pod 详情:

$ kubectl describe pod sleep-78ff5975c6-v6qg4
Name:             sleep-78ff5975c6-v6qg4
Namespace:        default
Priority:         0
Service Account:  sleep
Init Containers:
  istio-init:
    Container ID:  docker://c724b0c29ffd828e497cfdff45706061855b1b8c93073f9f037acc112367bceb
    Image:         docker.io/istio/proxyv2:1.17.1
Containers:
  sleep:
    Container ID:  docker://44f35a4934841c5618eb68fb5615d75ea5dd9c5dd826cb6a99a6ded6efaa6707
    Image:         curlimages/curl
  istio-proxy:
    Container ID:  docker://10a9ff35f45c7f07c1fcf88d4f8daa76282d09ad96912e026e59a4d0a99f02cf
    Image:         docker.io/istio/proxyv2:1.17.1

Events:
  Type     Reason     Age    From               Message
  ----     ------     ----   ----               -------
  Normal   Created    4m22s  kubelet            Created container istio-init
  Normal   Started    4m21s  kubelet            Started container istio-init
  Normal   Created    4m20s  kubelet            Created container sleep
  Normal   Started    4m20s  kubelet            Started container sleep
  Normal   Created    4m20s  kubelet            Created container istio-proxy
  Normal   Started    4m20s  kubelet            Started container istio-proxy

可以看到除了原始的 sleep 容器,多了一个 istio-proxy 容器,这就是边车代理,另外还多了一个 istio-init 初始化容器,它使用 iptables 将网络流量自动转发到边车代理,对应用程序完全透明。

另一种方法是手工注入边车代理,先将 default 命名空间的 istio-injection 标签移除:

$ kubectl label namespace default istio-injection-
namespace/default unlabeled

同时删除 sleep 应用:

$ kubectl delete -f samples/sleep/sleep.yaml
serviceaccount "sleep" deleted
service "sleep" deleted
deployment.apps "sleep" deleted

然后再重新部署 sleep 应用,并使用 istioctl kube-inject 命令手工注入边车代理:

$ istioctl kube-inject -f samples/sleep/sleep.yaml | kubectl apply -f -
serviceaccount/sleep created
service/sleep created
deployment.apps/sleep created

默认情况下,Istio 使用集群中的默认配置模版来生成边车配置,这个默认配置模版保存在名为 istio-sidecar-injector 的 ConfigMap 中:

# 配置模版
$ kubectl -n istio-system get configmap istio-sidecar-injector -o=jsonpath='{.data.config}'

# 配置值
$ kubectl -n istio-system get configmap istio-sidecar-injector -o=jsonpath='{.data.values}'

除了官方提供的默认模版,还可以通过在 Pod 中添加一个 istio-proxy 容器来自定义注入内容,或者通过 inject.istio.io/templates 注解来设置自定义模版,更多内容可以 参考官方文档

体验 Bookinfo 示例应用

这一节我们来部署一个更复杂的例子,在 samples/bookinfo 目录下是名为 Bookinfo 的示例应用,我们就使用这个应用来体验 Istio 的功能。为了方便起见,我们还是给 default 命名空间打上 istio-injection=enabled 标签开启自动注入功能,然后,执行下面的命令部署 Bookinfo 示例应用:

$ kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml
service/details created
serviceaccount/bookinfo-details created
deployment.apps/details-v1 created
service/ratings created
serviceaccount/bookinfo-ratings created
deployment.apps/ratings-v1 created
service/reviews created
serviceaccount/bookinfo-reviews created
deployment.apps/reviews-v1 created
deployment.apps/reviews-v2 created
deployment.apps/reviews-v3 created
service/productpage created
serviceaccount/bookinfo-productpage created
deployment.apps/productpage-v1 created

这是一个简单的在线书店应用,包含了四个微服务,这些微服务由不同的语言编写:

  • productpage - 产品页面,它会调用 detailsreviews 两个服务;
  • details - 书籍详情服务;
  • reviews - 书籍评论服务,它有三个不同的版本,v1 版本不包含评价信息,v2 和 v3 版本会调用 ratings 服务获取书籍评价,不过 v2 显示的是黑色星星,v3 显示的是红色星星;
  • ratings - 书籍评价服务;

部署时 Istio 会对每个微服务自动注入 Envoy 边车代理,我们可以通过 kubectl get pods 命令进行确认,确保每个 Pod 里都有两个容器在运行,其中一个是真实服务,另一个是代理服务:

$ kubectl get pods
NAME                             READY   STATUS    RESTARTS   AGE
details-v1-5ffd6b64f7-r6pr5      2/2     Running   0          14m
productpage-v1-979d4d9fc-gdmzh   2/2     Running   0          14m
ratings-v1-5f9699cfdf-trqj6      2/2     Running   0          14m
reviews-v1-569db879f5-gd9st      2/2     Running   0          14m
reviews-v2-65c4dc6fdc-5lph4      2/2     Running   0          14m
reviews-v3-c9c4fb987-kzjfk       2/2     Running   0          14m

部署之后整个系统的架构图如下所示:

bookinfo.png

这个时候从外部还不能访问该服务,只能在集群内访问,进入 ratings 服务所在的 Pod,验证 productpage 服务能否正常访问:

$ kubectl exec -it ratings-v1-5f9699cfdf-trqj6 -- sh
$ curl -s productpage:9080/productpage | grep "<title>"
    <title>Simple Bookstore App</title>

为了能从外部访问该服务,我们需要创建一个入站网关:

$ kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml
gateway.networking.istio.io/bookinfo-gateway created
virtualservice.networking.istio.io/bookinfo created

这个 YAML 中包含两个部分,第一部分定义了名为 bookinfo-gateway网关(Gateway)

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: bookinfo-gateway
spec:
  selector:
    istio: ingressgateway # use istio default controller
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"

注意这里使用了 istio-ingressgateway 作为网关,这在我们安装 Istio 的时候已经自动安装好了。

第二部分定义了名为 bookinfo虚拟服务(Virtual Service)

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: bookinfo
spec:
  hosts:
  - "*"
  gateways:
  - bookinfo-gateway
  http:
  - match:
    - uri:
        exact: /productpage
    - uri:
        prefix: /static
    - uri:
        exact: /login
    - uri:
        exact: /logout
    - uri:
        prefix: /api/v1/products
    route:
    - destination:
        host: productpage
        port:
          number: 9080

将这个虚拟服务和上面的网关关联起来,这样做的好处是,我们可以像管理网格中其他数据平面的流量一样去管理网关流量。

有了这个网关后,我们就可以在浏览器中输入 http://localhost/productpage 访问我们的在线书店了:

bookinfo-productpage.png

因为这里我们部署了三个版本的 reviews 服务,所以多刷新几次页面,可以看到页面上会随机展示 reviews 服务的不同版本的效果(可能不显示星星,也可能显示红色或黑色的星星)。

配置请求路径

由于部署了三个版本的 reviews 服务,所以每次访问应用页面看到的效果都不一样,如果我们想访问固定的版本,该怎么做呢?

检查上面的 bookinfo.yaml 文件,之所以每次访问的效果不一样,是因为 reviews 服务的 Service 使用了 app=reviews 选择器,所以请求会负载到所有打了 app=reviews 标签的 Pod:

apiVersion: v1
kind: Service
metadata:
  name: reviews
  labels:
    app: reviews
    service: reviews
spec:
  ports:
  - port: 9080
    name: http
  selector:
    app: reviews

reviews-v1reviews-v2reviews-v3 三个版本的 Deployment 都使用了 app=reviews 标签:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: reviews-v1
  labels:
    app: reviews
    version: v1
spec:
  ...
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: reviews-v2
  labels:
    app: reviews
    version: v2
spec:
  ...
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: reviews-v3
  labels:
    app: reviews
    version: v3
spec:
  ...

所以请求会负载到所有版本的 reviews 服务,这是 Kubernetes Service 的默认行为。不过要知道,现在所有 Pod 的流量都在我们的边车代理管控之下,我们当然可以使用 Istio 来改变请求的路径。首先,我们注意到这三个版本的 reviews 服务虽然 app=reviews 标签都一样,但是 version 标签是不一样的,这个特点可以用来定义 reviews目标规则(Destination Rules)

$ kubectl apply -f samples/bookinfo/networking/destination-rule-reviews.yaml
destinationrule.networking.istio.io/reviews created

destination-rule-reviews.yaml 文件内容如下:

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: reviews
spec:
  host: reviews
  trafficPolicy:
    loadBalancer:
      simple: RANDOM
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
  - name: v3
    labels:
      version: v3

在 Istio 里,目标规则和虚拟服务一样,是一个非常重要的概念。在目标规则中可以定义负载策略,并根据标签来将应用划分成不同的服务子集(subset),在上面的例子中,我们将 reviews 服务划分成了 v1v2v3 三个服务子集。接下来再为 reviews 定义一个虚拟服务:

$ kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-v3.yaml
virtualservice.networking.istio.io/reviews created

virtual-service-reviews-v3.yaml 文件内容如下:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
    - reviews
  http:
  - route:
    - destination:
        host: reviews
        subset: v3

在这个虚拟服务中我们指定了 v3 服务子集,稍等片刻,再次刷新页面,可以发现每次显示的都是红色的星星,我们成功的通过 Istio 控制了服务之间的流量。

基于匹配条件的路由

虚拟服务如果只能指定固定的服务子集,那么和 Kubernetes Service 也就没什么区别了。之所以在 Istio 引入虚拟服务的概念,就是为了 将客户端请求的目标地址与真实响应请求的目标工作负载进行解耦,这使得 A/B 测试很容易实现,比如我们可以让指定用户路由到特定的服务子集:

$ kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-jason-v2-v3.yaml
virtualservice.networking.istio.io/reviews configured

virtual-service-reviews-jason-v2-v3.yaml 文件内容如下:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
  - reviews
  http:
  - match:
    - headers:
        end-user:
          exact: jason
    route:
    - destination:
        host: reviews
        subset: v2
  - route:
    - destination:
        host: reviews
        subset: v3

在这个虚拟服务中,不仅定义了一个默认路由,目标是 reviews 的 v3 服务子集,而且还定义了一个目标是 v2 服务子集的路由,但是这个路由必须满足条件 headers.end-user.exact = jason,也就是 HTTP 请求头中必须包含值为 jasonend-user 字段,为了实现这一点,我们使用 jason 用户登录即可,这时看到的书籍评价就是黑色星星:

bookinfo-productpage-jason.png

退出登录后,看到的仍然是红色的星星,这样我们就实现了 A/B 测试的功能。

除了将 headers 作为匹配条件,我们还可以使用 urimethodqueryParams 等参数,具体内容可参考 HTTPMatchRequest

基于权重的路由

在上面的例子中我们实现了指定用户的 A/B 测试,一般来说,我们的服务上线后,先通过指定的测试账号进行验证,验证没有问题后就可以开始迁移流量了,我们可以先迁移 10% 的流量到新版本:

$ kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-90-10.yaml
virtualservice.networking.istio.io/reviews configured

virtual-service-reviews-90-10.yaml 文件内容如下:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
    - reviews
  http:
  - route:
    - destination:
        host: reviews
        subset: v1
      weight: 90
    - destination:
        host: reviews
        subset: v2
      weight: 10

在这个虚拟服务中,通过 weight 参数指定不同服务子集的权重。如果运行一段时间后没有问题,我们可以继续迁移 20% 的流量到新版本:

$ kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-80-20.yaml
virtualservice.networking.istio.io/reviews configured

然后继续迁移 50%,80%,直到 100% 的流量,至此服务升级就完成了,这种升级方式也被称为 金丝雀部署

其他流量管理功能

上面演示了如何使用 Istio 对不同版本的微服务进行路由配置,这是 Istio 流量管理的基本功能。除此之外,Istio 还提供了很多其他的 流量管理功能

可观测性

可观测性是服务网格的另一个重要特性,Istio 对很多开源组件提供了集成,比如使用 Prometheus 收集指标,使用 GrafanaKiali 对指标进行可视化,使用 JaegerZipkinApache SkyWalking 进行分布式追踪。

samples/addons 目录下提供了 Prometheus、Grafana、Kiali 和 Jaeger 这几个组件的精简版本,我们通过下面的命令安装:

$ kubectl apply -f samples/addons
serviceaccount/grafana created
configmap/grafana created
service/grafana created
deployment.apps/grafana created
configmap/istio-grafana-dashboards created
configmap/istio-services-grafana-dashboards created
deployment.apps/jaeger created
service/tracing created
service/zipkin created
service/jaeger-collector created
serviceaccount/kiali created
configmap/kiali created
clusterrole.rbac.authorization.k8s.io/kiali-viewer created
clusterrole.rbac.authorization.k8s.io/kiali created
clusterrolebinding.rbac.authorization.k8s.io/kiali created
role.rbac.authorization.k8s.io/kiali-controlplane created
rolebinding.rbac.authorization.k8s.io/kiali-controlplane created
service/kiali created
deployment.apps/kiali created
serviceaccount/prometheus created
configmap/prometheus created
clusterrole.rbac.authorization.k8s.io/prometheus created
clusterrolebinding.rbac.authorization.k8s.io/prometheus created
service/prometheus created
deployment.apps/prometheus created

安装完成后,使用下面的命令打开 Prometheus UI:

$ istioctl dashboard prometheus

在 Expression 对话框中输入指标名称即可查询相应的指标,比如查询 istio_requests_total 指标:

prometheus.png

使用下面的命令打开 Grafana UI:

$ istioctl dashboard grafana

Istio 内置了几个常用的 Grafana Dashboard 可以直接使用:

grafana.png

比如查看 Istio Control Plane Dashboard,这里显示的是控制平面 Istiod 的一些指标图:

grafana-ctl-plane-dashboard.png

要注意的是 Istio 默认的采样率为 1%,所以这时去查看 Istio Mesh Dashboard 还看不到数据,可以通过下面的脚本模拟 100 次请求:

$ for i in `seq 1 100`; do curl -s -o /dev/null http://localhost/productpage; done

使用下面的命令打开 Kiali UI:

$ istioctl dashboard kiali

选择左侧的 Graph 菜单,然后在 Namespace 下拉列表中选择 default,就可以看到 Bookinfo 示例应用的整体概览,以及各个服务之间的调用关系:

kiali.png

最后,使用下面的命令打开 Jaeger UI:

$ istioctl dashboard jaeger

从仪表盘左边面板的 Service 下拉列表中选择 productpage.default,并点击 Find Traces 按钮查询该服务的所有 Trace,随便点击一个 Trace,查看它的详细信息:

jaeger.png

上面对几种常见的可观测性组件做了一个大概的介绍,如果想进一步学习各个组件的高级特性,可以参考 Istio 的官方文档 以及各个组件的官方文档。

参考

更多

其他官方学习文档

Kubernetes Gateway API

在最早的版本中,Istio 使用 Kubernetes 提供的 Ingress API 来进行流量管理,但是这个 API 在管理大型应用系统或非 HTTP 协议服务时存在一定的缺点和限制,Istio 在 2018 年 推出了新的流量管理 API v1alpha3,在新的 API 中定义了下面这些资源:

  • Gateway
  • VirtualService
  • DestinationRule
  • ServiceEntry

关于这几个概念的意义,可以通过 Istio 的 官方文档 来学习。

2022 年 7 月,Kubernetes 发布了 Gateway API 的 Beta 版本,Istio 立即宣布了对 Kubernetes Gateway API 的支持,Istio API 也跟着升级到了 Beta 版本,并表示 Gateway API 将作为未来所有 Istio 流量管理的默认 API。

目前,用户可以选择 Kubernetes Ingress、Istio classic 和 Gateway API 三种方式来管理网格中的流量,相关内容可以参考 Kubernetes IngressIstio GatewayKubernetes Gateway API

扫描二维码,在手机上阅读!