禹鼎侯

禹鼎侯 查看完整档案

上海编辑安徽理工大学  |  自动化 编辑擎创信息  |  系统开发工程师 编辑 chenyc4.github.io/ 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

禹鼎侯 发布了文章 · 1月2日

魔性的float浮点数精度问题

从一个问题引入

如果你以前接触过C语言,那么对下面的这段代码一定很熟悉:

#include <stdio.h>

int main(void)
{
        float f_num1 = 21.75;
        float f_num2 = 13.45;
        printf("f_num1 = %f\n", f_num1);
        printf("f_num2 = %f\n", f_num2);
        printf("f_num1 + f_num2 = %f\n", f_num1 + f_num2);

        return 0;
}

相信很多人不用运行,能够直接报出答案, f_num1 = 21.75, f_num2 = 13.45, f_num1 + f_num2 = 35.2,无论是从常识还是理论角度都不难理解。
下面我们运行一下程序,验证我们的猜测正不正确:

f_num1 = 21.750000
f_num2 = 13.450000
f_num1 + f_num2 = 35.200001

f_num1f_num2的结果和我们预想的一样,之所以后面多了四个0,是因为%f默认保留6位有效数字。但是f_num1 + f_num2的结果是什么鬼,这个35.200001是从哪里来的?
是不是一下子颠覆了我们的认知?
惊不惊喜,意不意外,刺不刺激?是不是发现自从学了C语言,连简单的算术都不会算了?
别急,还有更令你崩溃的。

如果是C++呢

下面我们看看以上程序的C++版本:

#include<iostream>
using namespace std;

int main(void)
{
        float f_num1 = 21.75;
        float f_num2 = 13.45;
        cout << "f_num1 = " << f_num1 << endl;
        cout << "f_num2 = " << f_num2 << endl;
        cout << "f_num1 + f_num2 = " << f_num1 + f_num2 << endl;
        return 0;
}

直接来看输出结果吧:

f_num1 = 21.75
f_num2 = 13.45
f_num1 + f_num2 = 35.2

很神奇是不是?因为这个结果看起来正常多了。
看到这里,相信我们的心里都有老大一个疑问:为什么C程序和C++程序对同样的数字处理,输出的结果却不一样的?cout到底做了些什么?

cout的神奇之处

为了验证cout对浮点数的处理,我们不妨看一下下面的程序:

#include <iostream>
using namespace std;

int main(void)
{
        float num1 = 5;
        float num2 = 5.00;
        float num3 = 5.14;
        float num4 = 5.140000;
        float num5 = 5.123456;
        float num6 = 5.987654321;
        cout << "num1 = " << num1 << endl;
        cout << "num2 = " << num2 << endl;
        cout << "num3 = " << num3 << endl;
        cout << "num4 = " << num4 << endl;
        cout << "num5 = " << num5 << endl;
        cout << "num6 = " << num6 << endl;

        return 0;
}

看结果来分析比较直观,运行以上程序,结果如下:

num1 = 5
num2 = 5
num3 = 5.14
num4 = 5.14
num5 = 5.12346
num6 = 5.98765

num1num2num3num4这两组结果可以知道,cout对于float类型数值小数点后面的0是直接省去了的(这点和C语言格式化输出的%g有点像)。
num5num6两组结果不难分析出,cout对于浮点型数值,最多保留6位有效数字。
以上是cout处理浮点数时的特点,应该记住。
事实上,我们使用iostream库里的cout.setf不难使cout恢复精度。我们对上面的代码修改如下:

#include<iostream>
using namespace std;

int main(void)
{
        float f_num1 = 21.75;
        float f_num2 = 13.45;
        cout.setf(ios_base::fixed, ios_base::floatfield);       
        cout << "f_num1 = " << f_num1 << endl;
        cout << "f_num2 = " << f_num2 << endl;
        cout << "f_num1 + f_num2 = " << f_num1 + f_num2 << endl;
        return 0;
}

输出的结果就与C语言版本一模一样了:

f_num1 = 21.750000
f_num2 = 13.450000
f_num1 + f_num2 = 35.200001

答案呼之欲出

文章写到这里,相信你已经看出来问题的所在了。
不错,之所以结果不一样,正是由于精度引起的!
让我们回顾一下官方教材里关于float精度的描述:

浮点型和表示单精度、双精度和扩展精度值。C++标准指定了一个浮点数有效位数的最小值,然而大多数编译器都实现了更高的精度。 通常,float以一个字(32比特)来表示,double以2个字(64比特)来表示,long double 以3或4个字(96或128比特)来表示。一般来说,类型floatdouble分别有7和16个有效位;类型long double则常常被用于有特殊浮点需求的硬件,它的具体实现不同,精度也各不相同。(《C++ Primer第五版》

由以上描述,我们不难知道,对于float来说,最多只有7个有效位,这也就意味着,当实际存储的精度大于float的精度范围时,就会出现精度丢失现象。
为了进一步佐证上述问题,我们不妨将float的数值放大10亿倍,看看里面存储的值到底是多少:

#include<iostream>
using namespace std;

int main(void)
{
        float f_num1 = 21.75;
        float f_num2 = 13.45;
        cout.setf(ios_base::fixed, ios_base::floatfield);
        int billion = 1E9;
        float f_num10 = f_num1 * billion;
        float f_num20 = f_num2 * billion;
        cout << "f_num1 = " << f_num1 << endl;
        cout << "f_num2 = " << f_num2 << endl;

        cout << "f_num10 = " << f_num10 << endl;
        cout << "f_num20 = " << f_num20 << endl;
        return 0;
}

以上程序运行结果如下:

f_num1 = 21.750000
f_num2 = 13.450000
f_num10 = 21749999616.000000
f_num20 = 13449999360.000000

由此我们不难推断,21.75在实际存储时,并不是存储的21.75,而是21.749999616,同样的,12.45存储的是12.449999360,这样计算出来之后自然就会造成结果的不正确。

再看一个例子

我们再来看一个精度丢失造成运算结果不正确的例子。

#include<iostream>
using namespace std;

int main(void)
{
        float num1 = 2.3410E23;
        float num2 = num1 + 1.0f;
        cout << "num2 - num1 = " << num2 - num1 << endl;
        return 0;
}

如果精度不丢失,运算结果应该为1才对,可是因为精度丢失,导致最后的加1实际和没加效果一样,计算出来的结果是0。

num2 - num1 = 0

怎么解决

那么,既然float有这么多稀奇古怪的问题,应该怎么去解决和避免呢?

首先,当然推荐大家在编程时尽量使用高精度的浮点类型

比如double就比float精度要高,很多时候,使用double能够避免很多问题,比如本文一开始提到的问题,如果使用double就能完美解决:

#include <stdio.h>

int main(void)
{
        double f_num1 = 21.75;
        double f_num2 = 13.45;
        printf("f_num1 = %lf\n", f_num1);
        printf("f_num2 = %lf\n", f_num2);
        printf("f_num1 + f_num2 = %lf\n", f_num1 + f_num2);

        return 0;
}

大家可以自己运行一下看看结果。
double类型可以解决大部分精度丢失问题,基本上满足日常使用了,但是仍然不能避免精度丢失(double也有精度限制),这时候就需要想另外的方法来解决了。

万能的cout

前面提到过,cout其实是可以解决这种精度丢失问题的,所以如果不是对效率要求过高或者要求格式化输出(其实cout也可以实现格式化输出,此处不详细展开)必须使用printf,在编写C++程序时,建议使用cout代替printf

写在最后

本文只是简单的介绍了一下浮点型数值的精度问题,如果要深入细究,肯定不止这么多内容,比如浮点型数值在内存中是如何存储的?在字节里是如何分布 的?这才是真正核心的原理部分。在这里只浅尝辄止地讲述了一下,但相信阅读者已经对精度问题有了一个初步的认识。

查看原文

赞 0 收藏 0 评论 0

禹鼎侯 赞了回答 · 2020-12-25

gcc编译器,这样的随机数是如何产生的?

这并不是随机数,C 语言的 vararg(变长参数)不包含长度信息,所以即便你没传参数 printf 也不会知道,它仍然会傻傻地读取预定的位置,在 32 位下这个位置应该是 ebp + 12,在 64 位下则是 rsi 寄存器。

通常来讲这个内容不会变,至于为什么实际运行中在变,是因为 Linux 默认开启了名为 ASLR 的安全手段,在每次程序启动时都给程序基址附加了一个随机的偏移,提高内存漏洞的利用难度。将它关掉就会发现数字稳定下来了。

图片.png

这方面的知识可以搜索“C语言函数调用约定”来了解

关注 2 回答 1

禹鼎侯 发布了文章 · 2020-10-21

Logstash jdbc 按时间增量更新的一些总结

不同数据库的支持

mysql

数据类型显示样例是否支持timestampstatementtracking_columntracking_column_typeSQL示例
date2020-10-20Nselect *, datediff(date, '1970-01-01') as days from tbl_time where datediff(date, '1970-01-01') > :sql_last_valuedaysnumericselect *, datediff(date, '1970-01-01') as days from tbl_time where datediff(date, '1970-01-01') > 1603244266
datetime timestamp2020-10-20 06:12:01Yselect * from tbl_time where time > :sql_last_valuetimetimestampselect * from tbl_time where time > '2020-10-20 06:12:01'
时间戳1603244266Yselect *, FROM_UNIXTIME(shjnch, '%Y-%m-%d %h:%i:%s') as timestamp from tbl_time where FROM_UNIXTIME(shjnch, '%Y-%m-%d %h:%i:%s') > :sql_last_valuetimestamptimestampselect *, FROM_UNIXTIME(shjnch, '%Y-%m-%d %h:%i:%s') as timestamp from tbl_time where FROM_UNIXTIME(shjnch, '%Y-%m-%d %h:%i:%s') > '2020-10-21 14:00:00'

sqlserver

数据类型显示样例是否支持timestamp
date2020-10-21N
time14:00:00.0000000N
datetime2020-10-21 13:59:40.000Y
datetime22020-10-21 14:00:00Y
smalldatetime2020-10-21 14:00:00Y
datetimeoffset2020-10-21 14:00:00.0000000 +08:00Y

db2

数据类型显示样例是否支持timestamp
date2020-10-21N
time14:00:00.0000000N
timestamp2020-10-21 13:59:40.000Y

Oracle

数据类型显示样例是否支持timestampstatement
date2010-2-12N
timestamp12-FEB-10 01.24.52.234123211 PMYselect *,to_char(sysdate,'YYYY-MM-DD HH24:MI:SS') as time d1 from dual where to_char(sysdate,'YYYY-MM-DD HH24:MI:SS') > :sql_last_value

Postgre

数据类型显示样例是否支持timestamp
date1997-01-01N
timestamp2020-06-17 10:01:08.03282Y
time12:00:00N

sybase

数据类型显示样例是否支持timestamp
dateJul 24 2014N
timestamp0x00000000000a8b75N
datetime待验证
smalldatetime待验证

总结

通过测试,基本上可以断定,只要是时间格式为2020-10-21 14:00:00[.000]这种格式,都可以通过timestamp来实现时间增量更新。
对于不能通过这种方式的,看看有没有对应的函数进行转换成这种格式,如果没有,则只能采用datediff转换成天数(或秒数)之后通过numeric实现增量同步了。
如需要根据上述的date字段做增量同步,则可配置如下:

 statement => "select *, datediff(s, '1970-01-01', date)  from tbl_time where datediff(s, '1970-01-01', date) > :sql_last_value"
 tracking_column => "datediff(s, '1970-01-01', date)"
 tracking_column_type => "numeric"

配置示例

下面给出两个具体的配置示例:
MySQL数据库表结构如下:

mysql> desc tbl_time;
+-----------+--------------+------+-----+---------+-------+
| Field     | Type         | Null | Key | Default | Extra |
+-----------+--------------+------+-----+---------+-------+
| id        | int(11)      | NO   | PRI | NULL    |       |
| bigid     | bigint(20)   | YES  |     | NULL    |       |
| name      | varchar(255) | YES  |     | NULL    |       |
| date      | date         | YES  |     | NULL    |       |
| time      | datetime     | YES  |     | NULL    |       |
| timestamp | timestamp    | YES  |     | NULL    |       |
| shjnch    | bigint(255)  | YES  |     | NULL    |       |
+-----------+--------------+------+-----+---------+-------+
7 rows in set (0.00 sec)

表中有一条数据:

mysql> select * from tbl_time;
+----+-------+-------+------------+---------------------+---------------------+------------+
| id | bigid | name  | date       | time                | timestamp           | shjnch     |
+----+-------+-------+------------+---------------------+---------------------+------------+
|  1 |     1 | time1 | 2020-10-21 | 2020-10-21 09:37:31 | 2020-10-21 09:37:34 | 1603244266 |
+----+-------+-------+------------+---------------------+---------------------+------------+
1 row in set (0.00 sec)

为了能够查询出数据,我将增量查询的sql中的>号改成<号,便于看到效果。

input {
    jdbc {
        jdbc_driver_class => "com.mysql.jdbc.Driver"
        jdbc_connection_string => "jdbc:mysql://127.0.0.1:3306/mytest?useSSL=false"
        jdbc_user => "root"
        jdbc_password => "123456"
        statement => "select *, FROM_UNIXTIME(shjnch, '%Y-%m-%d %h:%i:%s') as timestamp from tbl_time where FROM_UNIXTIME(shjnch, '%Y-%m-%d %h:%i:%s') < :sql_last_value""
        schedule => "*/1 * * * *"
        connection_retry_attempts => 5
        connection_retry_attempts_wait_time => 1
        tracking_column => "timestamp"
        tracking_column_type => "timestamp"
        columns_charset => {
            "message" => "utf-8"
        }
        use_column_value => true
        lowercase_column_names => false
        record_last_run => true
        add_field => {
            "@topic" => "fc491237449424896"
            "@tags" => []
            "@ip" => "127.0.0.1"
        }
    }
}

通过kafka可消费到数据如下:

{"@topic":"fc491237449424896","timestamp":"2020-10-21 09:37:46","@ip":"127.0.0.1","date":"2020-10-21T00:00:00.000+08:00","bigid":1,"time":"2020-10-21T09:37:31.000+08:00","@timestamp":"2020-10-21T15:36:02.526+08:00","id":1,"shjnch":1603244266,"name":"time1"}
查看原文

赞 0 收藏 0 评论 0

禹鼎侯 赞了文章 · 2020-10-18

7天用Go动手写/从零实现RPC框架GeeRPC

geerpc.jpg

0 目录

1 谈谈 RPC 框架

RPC(Remote Procedure Call,远程过程调用)是一种计算机通信协议,允许调用不同进程空间的程序。RPC 的客户端和服务器可以在一台机器上,也可以在不同的机器上。程序员使用时,就像调用本地程序一样,无需关注内部的实现细节。

不同的应用程序之间的通信方式有很多,比如浏览器和服务器之间广泛使用的基于 HTTP 协议的 Restful API。与 RPC 相比,Restful API 有相对统一的标准,因而更通用,兼容性更好,支持不同的语言。HTTP 协议是基于文本的,一般具备更好的可读性。但是缺点也很明显:

  • Restful 接口需要额外的定义,无论是客户端还是服务端,都需要额外的代码来处理,而 RPC 调用则更接近于直接调用。
  • 基于 HTTP 协议的 Restful 报文冗余,承载了过多的无效信息,而 RPC 通常使用自定义的协议格式,减少冗余报文。
  • RPC 可以采用更高效的序列化协议,将文本转为二进制传输,获得更高的性能。
  • 因为 RPC 的灵活性,所以更容易扩展和集成诸如注册中心、负载均衡等功能。

2 RPC 框架需要解决什么问题

RPC 框架需要解决什么问题?或者我们换一个问题,为什么需要 RPC 框架?

我们可以想象下两台机器上,两个应用程序之间需要通信,那么首先,需要确定采用的传输协议是什么?如果这个两个应用程序位于不同的机器,那么一般会选择 TCP 协议或者 HTTP 协议;那如果两个应用程序位于相同的机器,也可以选择 Unix Socket 协议。传输协议确定之后,还需要确定报文的编码格式,比如采用最常用的 JSON 或者 XML,那如果报文比较大,还可能会选择 protobuf 等其他的编码方式,甚至编码之后,再进行压缩。接收端获取报文则需要相反的过程,先解压再解码。

解决了传输协议和报文编码的问题,接下来还需要解决一系列的可用性问题,例如,连接超时了怎么办?是否支持异步请求和并发?

如果服务端的实例很多,客户端并不关心这些实例的地址和部署位置,只关心自己能否获取到期待的结果,那就引出了注册中心(registry)和负载均衡(load balance)的问题。简单地说,即客户端和服务端互相不感知对方的存在,服务端启动时将自己注册到注册中心,客户端调用时,从注册中心获取到所有可用的实例,选择一个来调用。这样服务端和客户端只需要感知注册中心的存在就够了。注册中心通常还需要实现服务动态添加、删除,使用心跳确保服务处于可用状态等功能。

再进一步,假设服务端是不同的团队提供的,如果没有统一的 RPC 框架,各个团队的服务提供方就需要各自实现一套消息编解码、连接池、收发线程、超时处理等“业务之外”的重复技术劳动,造成整体的低效。因此,“业务之外”的这部分公共的能力,即是 RPC 框架所需要具备的能力。

3 关于 GeeRPC

Go 语言广泛地应用于云计算和微服务,成熟的 RPC 框架和微服务框架汗牛充栋。grpcrpcxgo-micro 等都是非常成熟的框架。一般而言,RPC 是微服务框架的一个子集,微服务框架可以自己实现 RPC 部分,当然,也可以选择不同的 RPC 框架作为通信基座。

考虑性能和功能,上述成熟的框架代码量都比较庞大,而且通常和第三方库,例如 protobufetcdzookeeper 等有比较深的耦合,难以直观地窥视框架的本质。GeeRPC 的目的是以最少的代码,实现 RPC 框架中最为重要的部分,帮助大家理解 RPC 框架在设计时需要考虑什么。代码简洁是第一位的,功能是第二位的。

因此,GeeRPC 选择从零实现 Go 语言官方的标准库 net/rpc,并在此基础上,新增了协议交换(protocol exchange)、注册中心(registry)、服务发现(service discovery)、负载均衡(load balance)、超时处理(timeout processing)等特性。分七天完成,最终代码约 1000 行。

附 推荐阅读

原文地址:7天用Go从零实现RPC框架GeeRPC - 极客兔兔
关注知乎:极客兔兔
关注微博:@极客兔兔

查看原文

赞 11 收藏 8 评论 2

禹鼎侯 关注了用户 · 2020-10-17

public0821 @public0821

关注 593

禹鼎侯 赞了文章 · 2020-10-17

Linux Cgroup系列(04):限制cgroup的内存使用(subsystem之memory)

有了上一篇关于pids的热身之后,我们这篇将介绍稍微复杂点的内存控制。

本篇所有例子都在ubuntu-server-x86_64 16.04下执行通过

为什么需要内存控制?

代码总会有bug,有时会有内存泄漏,或者有意想不到的内存分配情况,或者这是个恶意程序,运行起来就是为了榨干系统内存,让其它进程无法分配到足够的内存而出现异常,如果系统配置了交换分区,会导致系统大量使用交换分区,从而系统运行很慢。

  • 站在一个普通Linux开发者的角度,如果能控制一个或者一组进程所能使用的内存数,那么就算代码有bug,内存泄漏也不会对系统造成影响,因为可以设置内存使用量的上限,当到达这个值之后可以将进程重启。

  • 站在一个系统管理者的角度,如果能限制每组进程所能使用的内存量,那么不管程序的质量如何,都能将它们对系统的影响降到最低,从而保证整个系统的稳定性。

内存控制能控制些什么?

  • 限制cgroup中所有进程所能使用的物理内存总量

  • 限制cgroup中所有进程所能使用的物理内存+交换空间总量(CONFIG_MEMCG_SWAP): 一般在server上,不太会用到swap空间,所以不在这里介绍这部分内容。

  • 限制cgroup中所有进程所能使用的内核内存总量及其它一些内核资源(CONFIG_MEMCG_KMEM): 限制内核内存有什么用呢?其实限制内核内存就是限制当前cgroup所能使用的内核资源,比如进程的内核栈空间,socket所占用的内存空间等,通过限制内核内存,当内存吃紧时,可以阻止当前cgroup继续创建进程以及向内核申请分配更多的内核资源。由于这块功能被使用的较少,本篇中也不对它做介绍。

内核相关的配置

  • 由于memory subsystem比较耗资源,所以内核专门添加了一个参数cgroup_disable=memory来禁用整个memory subsystem,这个参数可以通过GRUB在启动系统的时候传给内核,加了这个参数后内核将不再进行memory subsystem相关的计算工作,在系统中也不能挂载memory subsystem。

  • 上面提到的CONFIG_MEMCG_SWAP和CONFIG_MEMCG_KMEM都是扩展功能,在使用前请确认当前内核是否支持,下面看看ubuntu 16.04的内核:

    #这里CONFIG_MEMCG_SWAP和CONFIG_MEMCG_KMEM等于y表示内核已经编译了该模块,即支持相关功能
    dev@dev:~$ cat /boot/config-`uname -r`|grep CONFIG_MEMCG
    CONFIG_MEMCG=y
    CONFIG_MEMCG_SWAP=y
    # CONFIG_MEMCG_SWAP_ENABLED is not set
    CONFIG_MEMCG_KMEM=y
  • CONFIG_MEMCG_SWAP控制内核是否支持Swap Extension,而CONFIG_MEMCG_SWAP_ENABLED(3.6以后的内核新加的参数)控制默认情况下是否使用Swap Extension,由于Swap Extension比较耗资源,所以很多发行版(比如ubuntu)默认情况下会禁用该功能(这也是上面那行被注释掉的原因),当然用户也可以根据实际情况,通过设置内核参数swapaccount=0或者1来手动禁用和启用Swap Extension。

怎么控制?

在ubuntu 16.04里面,systemd已经帮我们将memory绑定到了/sys/fs/cgroup/memory

#如果这里发现有多行结果,说明这颗cgroup数被绑定到了多个地方,
#不过不要担心,由于它们都是指向同一颗cgroup树,所以它们里面的内容是一模一样的
dev@dev:~$ mount|grep memory
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)

创建子cgroup

在/sys/fs/cgroup/memory下创建一个子目录即创建了一个子cgroup

#--------------------------第一个shell窗口----------------------
dev@dev:~$ cd /sys/fs/cgroup/memory
dev@dev:/sys/fs/cgroup/memory$ sudo mkdir test
dev@dev:/sys/fs/cgroup/memory$ ls test
cgroup.clone_children  memory.kmem.failcnt             memory.kmem.tcp.limit_in_bytes      memory.max_usage_in_bytes        memory.soft_limit_in_bytes  notify_on_release
cgroup.event_control   memory.kmem.limit_in_bytes      memory.kmem.tcp.max_usage_in_bytes  memory.move_charge_at_immigrate  memory.stat                 tasks
cgroup.procs           memory.kmem.max_usage_in_bytes  memory.kmem.tcp.usage_in_bytes      memory.numa_stat                 memory.swappiness
memory.failcnt         memory.kmem.slabinfo            memory.kmem.usage_in_bytes          memory.oom_control               memory.usage_in_bytes
memory.force_empty     memory.kmem.tcp.failcnt         memory.limit_in_bytes               memory.pressure_level            memory.use_hierarchy

从上面ls的输出可以看出,除了每个cgroup都有的那几个文件外,和memory相关的文件还不少(由于ubuntu默认禁用了CONFIG_MEMCG_SWAP,所以这里看不到swap相关的文件),这里先做个大概介绍(kernel相关的文件除外),后面会详细介绍每个文件的作用

 cgroup.event_control       #用于eventfd的接口
 memory.usage_in_bytes      #显示当前已用的内存
 memory.limit_in_bytes      #设置/显示当前限制的内存额度
 memory.failcnt             #显示内存使用量达到限制值的次数
 memory.max_usage_in_bytes  #历史内存最大使用量
 memory.soft_limit_in_bytes #设置/显示当前限制的内存软额度
 memory.stat                #显示当前cgroup的内存使用情况
 memory.use_hierarchy       #设置/显示是否将子cgroup的内存使用情况统计到当前cgroup里面
 memory.force_empty         #触发系统立即尽可能的回收当前cgroup中可以回收的内存
 memory.pressure_level      #设置内存压力的通知事件,配合cgroup.event_control一起使用
 memory.swappiness          #设置和显示当前的swappiness
 memory.move_charge_at_immigrate #设置当进程移动到其他cgroup中时,它所占用的内存是否也随着移动过去
 memory.oom_control         #设置/显示oom controls相关的配置
 memory.numa_stat           #显示numa相关的内存

参考:eventfdnuma

添加进程

“创建并管理cgroup”中介绍的一样,往cgroup中添加进程只要将进程号写入cgroup.procs就可以了

注意:本篇将以进程为单位进行操作,不考虑以线程为单位进行管理(原因见“创建并管理cgroup”中cgroup.pro与tasks的区别),也即只写cgroup.procs文件,不会写tasks文件

#--------------------------第二个shell窗口----------------------
#重新打开一个shell窗口,避免相互影响
dev@dev:~$ cd /sys/fs/cgroup/memory/test/
dev@dev:/sys/fs/cgroup/memory/test$ echo $$
4589
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo $$ >> cgroup.procs"
#运行top命令,这样这个cgroup消耗的内存会多点,便于观察
dev@dev:/sys/fs/cgroup/memory/test$ top
#后续操作不再在这个窗口进行,避免在这个bash中运行进程影响cgropu里面的进程数及相关统计

设置限额

设置限额很简单,写文件memory.limit_in_bytes就可以了,请仔细看示例

#--------------------------第一个shell窗口----------------------
#回到第一个shell窗口
dev@dev:/sys/fs/cgroup/memory$ cd test
#这里两个进程id分别时第二个窗口的bash和top进程
dev@dev:/sys/fs/cgroup/memory/test$ cat cgroup.procs
4589
4664
#开始设置之前,看看当前使用的内存数量,这里的单位是字节
dev@dev:/sys/fs/cgroup/memory/test$ cat memory.usage_in_bytes
835584

#设置1M的限额
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 1M > memory.limit_in_bytes"
#设置完之后记得要查看一下这个文件,因为内核要考虑页对齐, 所以生效的数量不一定完全等于设置的数量
dev@dev:/sys/fs/cgroup/memory/test$ cat memory.limit_in_bytes
1048576

#如果不再需要限制这个cgroup,写-1到文件memory.limit_in_bytes即可
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo -1 > memory.limit_in_bytes"
#这时可以看到limit被设置成了一个很大的数字
dev@dev:/sys/fs/cgroup/memory/test$ cat memory.limit_in_bytes
9223372036854771712

如果设置的限额比当前已经使用的内存少呢?如上面显示当前bash用了800多k,如果我设置limit为400K会怎么样?

#--------------------------第一个shell窗口----------------------
#先用free看下当前swap被用了多少
dev@dev:/sys/fs/cgroup/memory/test$ free
              total        used        free      shared  buff/cache   available
Mem:         500192       45000       34200        2644      420992      424020
Swap:        524284          16      524268
#设置内存限额为400K
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 400K > memory.limit_in_bytes"

#再看当前cgroup的内存使用情况
#发现内存占用少了很多,刚好在400K以内,原来用的那些内存都去哪了呢?
dev@dev:/sys/fs/cgroup/memory/test$ cat memory.usage_in_bytes
401408

#再看swap空间的占用情况,和刚开始比,多了500-16=384K,说明内存中的数据被移到了swap上
dev@dev:/sys/fs/cgroup/memory/test$ free
              total        used        free      shared  buff/cache   available
Mem:         500192       43324       35132        2644      421736      425688
Swap:        524284         500      523784

#这个时候再来看failcnt,发现有453次之多(隔几秒再看这个文件,发现次数在增长)
dev@dev:/sys/fs/cgroup/memory/test$ cat memory.failcnt
453

#再看看memory.stat(这里只显示部分内容),发现物理内存用了400K,
#但有很多pgmajfault以及pgpgin和pgpgout,说明发生了很多的swap in和swap out
dev@dev:/sys/fs/cgroup/memory/test$ cat memory.stat
rss 409600
total_pgpgin 4166
total_pgpgout 4066
total_pgfault 7558
total_pgmajfault 419

#从上面的结果可以看出,当物理内存不够时,就会触发memory.failcnt里面的数量加1,
#但进程不会被kill掉,那是因为内核会尝试将物理内存中的数据移动到swap空间中,从而让内存分配成功

如果设置的限额过小,就算swap out部分内存后还是不够会怎么样?

#--------------------------第一个shell窗口----------------------
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 1K > memory.limit_in_bytes"
#进程已经不在了(第二个窗口已经挂掉了)
dev@dev:/sys/fs/cgroup/memory/test$ cat cgroup.procs
dev@dev:/sys/fs/cgroup/memory/test$ cat memory.usage_in_bytes
0
#从这里的结果可以看出,第二个窗口的bash和top都被kill掉了

从上面的这些测试可以看出,一旦设置了内存限制,将立即生效,并且当物理内存使用量达到limit的时候,memory.failcnt的内容会加1,但这时进程不一定就会被kill掉,内核会尽量将物理内存中的数据移到swap空间上去,如果实在是没办法移动了(设置的limit过小,或者swap空间不足),默认情况下,就会kill掉cgroup里面继续申请内存的进程。

触发控制

当物理内存达到上限后,系统的默认行为是kill掉cgroup中继续申请内存的进程,那么怎么控制这样的行为呢?答案是配置memory.oom_control

这个文件里面包含了一个控制是否为当前cgroup启动OOM-killer的标识。如果写0到这个文件,将启动OOM-killer,当内核无法给进程分配足够的内存时,将会直接kill掉该进程;如果写1到这个文件,表示不启动OOM-killer,当内核无法给进程分配足够的内存时,将会暂停该进程直到有空余的内存之后再继续运行;同时,memory.oom_control还包含一个只读的under_oom字段,用来表示当前是否已经进入oom状态,也即是否有进程被暂停了。

注意:root cgroup的oom killer是不能被禁用的

为了演示OOM-killer的功能,创建了下面这样一个程序,用来向系统申请内存,它会每秒消耗1M的内存。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define MB (1024 * 1024)

int main(int argc, char *argv[])
{
    char *p;
    int i = 0;
    while(1) {
        p = (char *)malloc(MB);
        memset(p, 0, MB);
        printf("%dM memory allocated\n", ++i);
        sleep(1);
    }

    return 0;
}

保存上面的程序到文件~/mem-allocate.c,然后编译并测试

#--------------------------第一个shell窗口----------------------
#编译上面的文件
dev@dev:/sys/fs/cgroup/memory/test$ gcc ~/mem-allocate.c -o ~/mem-allocate
#设置内存限额为5M
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 5M > memory.limit_in_bytes"
#将当前bash加入到test中,这样这个bash创建的所有进程都会自动加入到test中
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo $$ >> cgroup.procs"
#默认情况下,memory.oom_control的值为0,即默认启用oom killer
dev@dev:/sys/fs/cgroup/memory/test$ cat memory.oom_control
oom_kill_disable 0
under_oom 0
#为了避免受swap空间的影响,设置swappiness为0来禁止当前cgroup使用swap
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 0 > memory.swappiness"
#当分配第5M内存时,由于总内存量超过了5M,所以进程被kill了
dev@dev:/sys/fs/cgroup/memory/test$ ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated
Killed

#设置oom_control为1,这样内存达到限额的时候会暂停
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 1 >> memory.oom_control"
#跟预期的一样,程序被暂停了
dev@dev:/sys/fs/cgroup/memory/test$ ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated

#--------------------------第二个shell窗口----------------------
#再打开一个窗口
dev@dev:~$ cd /sys/fs/cgroup/memory/test/
#这时候可以看到memory.oom_control里面under_oom的值为1,表示当前已经oom了
dev@dev:/sys/fs/cgroup/memory/test$ cat memory.oom_control
oom_kill_disable 1
under_oom 1
#修改test的额度为7M
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 7M > memory.limit_in_bytes"

#--------------------------第一个shell窗口----------------------
#再回到第一个窗口,会发现进程mem-allocate继续执行了两步,然后暂停在6M那里了
dev@dev:/sys/fs/cgroup/memory/test$ ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated
5M memory allocated
6M memory allocated

该文件还可以配合cgroup.event_control实现OOM的通知,当OOM发生时,可以收到相关的事件,下面是用于测试的程序,流程大概如下:

  1. 利用函数eventfd()创建一个efd;

  2. 打开文件memory.oom_control,得到ofd;

  3. 往cgroup.event_control中写入这么一串:<efd> <ofd>

  4. 通过读efd得到通知,然后打印一句话到终端

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/eventfd.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static inline void die(const char *msg)
{
    fprintf(stderr, "error: %s: %s(%d)\n", msg, strerror(errno), errno);
    exit(EXIT_FAILURE);
}

#define BUFSIZE 256

int main(int argc, char *argv[])
{
    char buf[BUFSIZE];
    int efd, cfd, ofd;
    uint64_t u;

    if ((efd = eventfd(0, 0)) == -1)
        die("eventfd");

    snprintf(buf, BUFSIZE, "%s/%s", argv[1], "cgroup.event_control");
    if ((cfd = open(buf, O_WRONLY)) == -1)
        die("cgroup.event_control");

    snprintf(buf, BUFSIZE, "%s/%s", argv[1], "memory.oom_control");
    if ((ofd = open(buf, O_RDONLY)) == -1)
        die("memory.oom_control");

    snprintf(buf, BUFSIZE, "%d %d", efd, ofd);
    if (write(cfd, buf, strlen(buf)) == -1)
        die("write cgroup.event_control");

    if (close(cfd) == -1)
        die("close cgroup.event_control");

    for (;;) {
        if (read(efd, &u, sizeof(uint64_t)) != sizeof(uint64_t))
            die("read eventfd");
        printf("mem_cgroup oom event received\n");
    }

    return 0;
}

将上面的文件保存为~/oom_listen.c,然后测试如下

#--------------------------第二个shell窗口----------------------
#编译程序
dev@dev:/sys/fs/cgroup/memory/test$ gcc ~/oom_listen.c -o ~/oom_listen
#启用oom killer
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 0 >> memory.oom_control"
#设置限额为2M,缩短测试周期
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 2M > memory.limit_in_bytes"
#启动监听程序
dev@dev:/sys/fs/cgroup/memory/test$ ~/oom_listen /sys/fs/cgroup/memory/test

#--------------------------第一个shell窗口----------------------
#连续运行两次mem-allocate,使它触发oom killer
dev@dev:/sys/fs/cgroup/memory/test$ ~/mem-allocate
1M memory allocated
Killed
dev@dev:/sys/fs/cgroup/memory/test$ ~/mem-allocate
1M memory allocated
Killed

#--------------------------第二个shell窗口----------------------
#回到第二个窗口可以看到,收到了两次oom事件
dev@dev:/sys/fs/cgroup/memory/test$ ~/oom_listen /sys/fs/cgroup/memory/test
mem_cgroup oom event received
mem_cgroup oom event received

其他

进程迁移(migration)

当一个进程从一个cgroup移动到另一个cgroup时,默认情况下,该进程已经占用的内存还是统计在原来的cgroup里面,不会占用新cgroup的配额,但新分配的内存会统计到新的cgroup中(包括swap out到交换空间后再swap in到物理内存中的部分)。

我们可以通过设置memory.move_charge_at_immigrate让进程所占用的内存随着进程的迁移一起迁移到新的cgroup中。

enable: echo 1 > memory.move_charge_at_immigrate
disable:echo 0 > memory.move_charge_at_immigrate

注意: 就算设置为1,但如果不是thread group的leader,这个task占用的内存也不能被迁移过去。换句话说,如果以线程为单位进行迁移,必须是进程的第一个线程,如果以进程为单位进行迁移,就没有这个问题。

当memory.move_charge_at_immigrate被设置成1之后,进程占用的内存将会被统计到目的cgroup中,如果目的cgroup没有足够的内存,系统将尝试回收目的cgroup的部分内存(和系统内存紧张时的机制一样,删除不常用的file backed的内存或者swap out到交换空间上,请参考Linux内存管理),如果回收不成功,那么进程迁移将失败。

注意:迁移内存占用数据是比较耗时的操作。

移除cgroup

当memory.move_charge_at_immigrate为0时,就算当前cgroup中里面的进程都已经移动到其它cgropu中去了,由于进程已经占用的内存没有被统计过去,当前cgroup有可能还占用很多内存,当移除该cgroup时,占用的内存需要统计到谁头上呢?答案是依赖memory.use_hierarchy的值,如果该值为0,将会统计到root cgroup里;如果值为1,将统计到它的父cgroup里面。

force_empty

当向memory.force_empty文件写入0时(echo 0 > memory.force_empty),将会立即触发系统尽可能的回收该cgroup占用的内存。该功能主要使用场景是移除cgroup前(cgroup中没有进程),先执行该命令,可以尽可能的回收该cgropu占用的内存,这样迁移内存的占用数据到父cgroup或者root cgroup时会快些。

memory.swappiness

该文件的值默认和全局的swappiness(/proc/sys/vm/swappiness)一样,修改该文件只对当前cgroup生效,其功能和全局的swappiness一样,请参考Linux交换空间中关于swappiness的介绍。

注意:有一点和全局的swappiness不同,那就是如果这个文件被设置成0,就算系统配置的有交换空间,当前cgroup也不会使用交换空间。

memory.use_hierarchy

该文件内容为0时,表示不使用继承,即父子cgroup之间没有关系;当该文件内容为1时,子cgroup所占用的内存会统计到所有祖先cgroup中。

如果该文件内容为1,当一个cgroup内存吃紧时,会触发系统回收它以及它所有子孙cgroup的内存。

注意: 当该cgroup下面有子cgroup或者父cgroup已经将该文件设置成了1,那么当前cgroup中的该文件就不能被修改。

#当前cgroup和父cgroup里都是1
dev@dev:/sys/fs/cgroup/memory/test$ cat memory.use_hierarchy
1
dev@dev:/sys/fs/cgroup/memory/test$ cat ../memory.use_hierarchy
1

#由于父cgroup里面的值为1,所以修改当前cgroup的值失败
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 0 > ./memory.use_hierarchy"
sh: echo: I/O error

#由于父cgroup里面有子cgroup(至少有当前cgroup这么一个子cgroup),
#修改父cgroup里面的值也失败
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 0 > ../memory.use_hierarchy"
sh: echo: I/O error

memory.soft_limit_in_bytes

有了hard limit(memory.limit_in_bytes),为什么还要soft limit呢?hard limit是一个硬性标准,绝对不能超过这个值,而soft limit可以被超越,既然能被超越,要这个配置还有啥用?先看看它的特点

  1. 当系统内存充裕时,soft limit不起任何作用

  2. 当系统内存吃紧时,系统会尽量的将cgroup的内存限制在soft limit值之下(内核会尽量,但不100%保证)

从它的特点可以看出,它的作用主要发生在系统内存吃紧时,如果没有soft limit,那么所有的cgroup一起竞争内存资源,占用内存多的cgroup不会让着内存占用少的cgroup,这样就会出现某些cgroup内存饥饿的情况。如果配置了soft limit,那么当系统内存吃紧时,系统会让超过soft limit的cgroup释放出超过soft limit的那部分内存(有可能更多),这样其它cgroup就有了更多的机会分配到内存。

从上面的分析看出,这其实是系统内存不足时的一种妥协机制,给次等重要的进程设置soft limit,当系统内存吃紧时,把机会让给其它重要的进程。

注意: 当系统内存吃紧且cgroup达到soft limit时,系统为了把当前cgroup的内存使用量控制在soft limit下,在收到当前cgroup新的内存分配请求时,就会触发回收内存操作,所以一旦到达这个状态,就会频繁的触发对当前cgroup的内存回收操作,会严重影响当前cgroup的性能。

memory.pressure_level

这个文件主要用来监控当前cgroup的内存压力,当内存压力大时(即已使用内存快达到设置的限额),在分配内存之前需要先回收部分内存,从而影响内存分配速度,影响性能,而通过监控当前cgroup的内存压力,可以在有压力的时候采取一定的行动来改善当前cgroup的性能,比如关闭当前cgroup中不重要的服务等。目前有三种压力水平:

low

意味着系统在开始为当前cgroup分配内存之前,需要先回收内存中的数据了,这时候回收的是在磁盘上有对应文件的内存数据。

medium

意味着系统已经开始频繁为当前cgroup使用交换空间了。

critical

快撑不住了,系统随时有可能kill掉cgroup中的进程。

如何配置相关的监听事件呢?和memory.oom_control类似,大概步骤如下:

  1. 利用函数eventfd(2)创建一个event_fd

  2. 打开文件memory.pressure_level,得到pressure_level_fd

  3. 往cgroup.event_control中写入这么一串:<event_fd> <pressure_level_fd> <level>

  4. 然后通过读event_fd得到通知

注意: 多个level可能要创建多个event_fd,好像没有办法共用一个(本人没有测试过)

Memory thresholds

我们可以通过cgroup的事件通知机制来实现对内存的监控,当内存使用量穿过(变得高于或者低于)我们设置的值时,就会收到通知。使用方法和memory.oom_control类似,大概步骤如下:

  1. 利用函数eventfd(2)创建一个event_fd

  2. 打开文件memory.usage_in_bytes,得到usage_in_bytes_fd

  3. 往cgroup.event_control中写入这么一串:<event_fd> <usage_in_bytes_fd> <threshold>

  4. 然后通过读event_fd得到通知

stat file

这个文件包含的统计项比较细,需要一些内核的内存管理知识才能看懂,这里就不介绍了(怕说错)。详细信息可以参考Memory Resource Controller中的“5.2 stat file”。这里有几个需要注意的地方:

  • 里面total开头的统计项包含了子cgroup的数据(前提条件是memory.use_hierarchy等于1)。

  • 里面的'rss + file_mapped"才约等于是我们常说的RSS(ps aux命令看到的RSS)

  • 文件(动态库和可执行文件)及共享内存可以在多个进程之间共享,不过它们只会统计到他们的owner cgroup中的file_mapped去。(不确定是怎么定义owner的,但如果看到当前cgroup的file_mapped值很小,说明共享的数据没有算到它头上,而是其它的cgroup)

结束语

本篇没有介绍swap和kernel相关的内容,不过在实际使用过程中一定要留意swap空间,如果系统使用了交换空间,那么设置限额时一定要注意一点,那就是当cgroup的物理空间不够时,内核会将不常用的内存swap out到交换空间上,从而导致一直不触发oom killer,而是不停的swap out/in,导致cgroup中的进程运行速度很慢。如果一定要用交换空间,最好的办法是限制swap+物理内存的额度,虽然我们在这篇中没有介绍这部分内容,但其使用方法和限制物理内存是一样的,只是换做写文件memory.memsw.limit_in_bytes罢了。

参考

查看原文

赞 26 收藏 17 评论 6

禹鼎侯 发布了文章 · 2020-10-16

cgroup内存限制不起作用的原因

今天遇到一个cgroup资源限制时,内存限制不起作用的问题。一开始,对进程的cgroup设置最大内存限制为10M,但运行了几分钟以后,内存明显飙上去了,甚至达到了20多M。

情景还原

memory.limit_in_bytes中设置如下:
image.png
10485760表示的是字节数,也就是等于 1024 * 1024 * 10 = 10M
运行一段时间后,查看实际使用的内存,memory.usage_in_bytes文件中数值如下:
image.png
可以看到,确实拿捏得死死的,看起来似乎内存限制成功了,没有什么问题。
可是当使用top命令去查看的时候,却发现已经内存消耗已高达23M左右,这明显不正常。
image.png
使用前台监控界面查看,得到的是同样的结果:
image.png
可见,虽然设置了cgroup,而且看起来似乎是生效了,但实际上并没有限制住。

问题排查

因为该程序的资源限制是利用agent代理程序去做的,并非人为去操作,所以一开始怀疑该进程没有加入cgroup资源限制策略,查看之后排除了这一可能性。
第一步查到进程PID以及其相应的线程ID,如下所示:
image.png
然后查看tasks里面加入的进程号,如下:
image.png
可以看到,该进程所有的进程和线程号都被加入到了cgroup中。因此,并不是这个引起的。
然后就想到会不会是使用了swap内存,可以看到,8G的swap内存已经被使用了24M,看起来有点像。
image.png
为了验证这一猜想,我把进程先给停掉了,然后查看swap内存使用情况:
image.png
可以看到,flow进程停掉之后,swap内存一下子从24M降到了20K,看样子就是这个东西搞的鬼。
于是我查看了一下memory.swappiness,这个数值居然达到了30,看来是这个问题没跑了。
image.png

问题解决

知道了问题所在,解决也就比较简单了。首先第一步,把memory.swappiness设为0

$ echo 0 >  memory.swappiness

然后重新启动进程,这次成功了,当内存刚刚达到10M,直接就被kill掉了。
但实际上,我并不希望这个进程就如此简单粗暴的被杀掉,考虑到memory.oom_control可以设置内存达到限制后的处理措施,oom_kill_disable0代表内存超过限制就杀掉进程,oom_kill_disable1则代表继续等待,当有内存释放时,继续申请内存。总而言之,就是不会把进程杀掉。
image.png
所以,将oom_kill_disable设置为1后重启程序,该问题得以解决。

总结

cgroupLinux系统内核提供的一种资源限制的策略,使用起来十分方便。鼎鼎大名的Docker容器就是基于此技术,平时工作中对这种技术缺乏钻研和积累,只知道简单的使用,并没有深入研究每个参数到底代表什么,其内部原理又是什么,所以才有了遇到这种问题耗时耗力的情况。
比如memory.swappiness中从数值代表什么意思,为啥设置为0之后cgroup就起作用了?原来的30又代表什么?
通过查资料后,了解到memory.swappiness中的数值其实并不是确切的数值,而是代表了进程使用swap空间的一个权重,该数值范围从0-100100表示积极使用swap0表示优先使用内存。

参考资料:docker cgroup 技术之memory(首篇)

查看原文

赞 0 收藏 0 评论 0

禹鼎侯 赞了文章 · 2020-10-11

Sentinel-Go 集成 Nacos 实现外部动态数据源

9.28头图.png

导读:2020年,Sentinel 推出 Go 原生版本Sentinel-Golang,在云原生领域继续突破。本文将从实际出发 结合案例说明 在Sentinel-Golang中如何集成Nacos,使其做为外部动态数据源,将流控规则存储在nacos中,并且实现动态实时更新规则。

本文主要分为两个部分:

  1. 将sentinel流控规则定义在代码内部 实现限流效果。
  2. 将sentinel流控规则定义在nacos配置中心,实现限流效果以及在nacos中动态更新规则,实现动态流控。

下面将详细介绍一下相关的背景知识。

1. Sentinel

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

Sentinel 具有以下特征:

  • 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
  • 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
  • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
  • 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

1.1 Sentinel 的历史

  • 2012年,Sentinel 诞生,主要功能为入口流量控制。
  • 2013-2017年,Sentinel 在阿里巴巴集团内部迅速发展,成为基础技术模块,覆盖了所有的核心场景。Sentinel 也因此积累了大量的流量归整场景以及生产实践。
  • 2018年,Sentinel 开源,并持续演进。
  • 2019年,Sentinel 在多语言扩展的方向上逐步探索,陆续推出 C++ 原生版本、Envoy 集群流量控制支持。
  • 2020年,Sentinel 推出 Go 原生版本,期待在云原生领域继续突破。https://github.com/alibaba/sentinel-golang

2. Nacos

Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理的平台,Nacos脱胎于阿里巴巴内部的ConfigServer和Diamond,是它们的开源实现。经历过双十一流量峰值和阿里巴巴经济体超大规模容量的考验,沉淀了阿里巴巴软负载团队在这个领域十年的经验,在稳定性和功能性上都有很好的保障。

1.png
(Sentinel-Go集成Nacos动态数据源架构)

目前 Sentinel 内部的限流、熔断等策略都是基于规则来实现的,提供动态数据源扩展的目的,就是希望将规则数据的加载以及更新操作通过一些配置中心中间件(比如 nacos,etcd,conful,等等)来实现动态更新。

3. Sentinel-Go 限流 Demo

未集成nacos时 规则定义在代码内部,没有使用外部数据源。

3.1 安装

go get github.com/alibaba/sentinel-golang

3.2 Demo样例

使用 Sentinel 主要分为以下几步:

  1. 对 Sentinel 进行相关配置并进行初始化
  2. 埋点(定义资源)
  3. 配置规则
package main

import (
    "fmt"
    "log"
    "math/rand"
    "time"

    sentinel "github.com/alibaba/sentinel-golang/api"
    "github.com/alibaba/sentinel-golang/core/base"
    "github.com/alibaba/sentinel-golang/core/flow"
    "github.com/alibaba/sentinel-golang/util"
)

func main() {
    // We should initialize Sentinel first.
    err := sentinel.InitDefault()
    if err != nil {
        log.Fatalf("Unexpected error: %+v", err)
    }

    _, err = flow.LoadRules([]*flow.FlowRule{
        {
            Resource:        "some-test",
            MetricType:      flow.QPS,
            Count:           10,
            ControlBehavior: flow.Reject,
        },
    })
    if err != nil {
        log.Fatalf("Unexpected error: %+v", err)
        return
    }

    ch := make(chan struct{})

    for i := 0; i < 10; i++ {
        go func() {
            for {
                e, b := sentinel.Entry("some-test", sentinel.WithTrafficType(base.Inbound))
                if b != nil {
                    // Blocked. We could get the block reason from the BlockError.
                    time.Sleep(time.Duration(rand.Uint64()%10) * time.Millisecond)
                } else {
                    // Passed, wrap the logic here.
                    fmt.Println(util.CurrentTimeMillis(), "passed")
                    time.Sleep(time.Duration(rand.Uint64()%10) * time.Millisecond)

                    // Be sure the entry is exited finally.
                    e.Exit()
                }

            }
        }()
    }
    <-ch
}

官方expmale:https://github.com/alibaba/sentinel-golang/tree/master/example

4. Sentinel-Go 集成Nacos

Sentinel-Go集成Nacos实现外部动态数据源功能.

4.1 部署Nacos

4.1.1 版本选择

您可以在Nacos的release notes博客中找到每个版本支持的功能的介绍,当前推荐的稳定版本为1.3.1。

4.1.2 预备环境准备

Nacos 依赖 Java 环境来运行。如果您是从代码开始构建并运行Nacos,还需要为此配置 Maven环境,请确保是在以下版本环境中安装使用:

  1. 64 bit OS,支持 Linux/Unix/Mac/Windows,推荐选用 Linux/Unix/Mac。
  2. 64 bit JDK 1.8+;下载 & 配置
  3. Maven 3.2.x+;下载 & 配置

4.1.3 下载源码或者安装包

你可以通过源码和发行包两种方式来获取 Nacos。

从 Github 上下载源码方式

git clone https://github.com/alibaba/nacos.git
cd nacos/
mvn -Prelease-nacos -Dmaven.test.skip=true clean install -U  
ls -al distribution/target/
// change the $version to your actual path
cd distribution/target/nacos-server-$version/nacos/bin

下载编译后压缩包方式

您可以从 最新稳定版本 下载 nacos-server-$version.zip 包。

unzip nacos-server-$version.zip 或者 tar -xvf nacos-server-$version.tar.gz
  cd nacos/bin

4.1.4 启动服务器

Linux/Unix/Mac
启动命令(standalone代表着单机模式运行,非集群模式):
sh startup.sh -m standalone
如果您使用的是ubuntu系统,或者运行脚本报错提示[[符号找不到,可尝试如下运行:
bash startup.sh -m standalone

Windows
启动命令:
cmd startup.cmd
或者双击startup.cmd运行文件。

部署成功访问 http://127.0.0.1:8848/nacos  
用户名/密码:nacos/nacos

4.2 Sentinel限流配置到Nacos

  1. 登录到nacos web
  2. 在配置管理中,新建配置
  3. 输入dataId,group(dataId,group 创建时可以自定义,本文创建的dataId=flow,group=sentinel-go)
  4. 将数据源样例粘贴到配置内容中。

4.2.1 Nacos 外部数据源样例

此样例是流量控制的Demo配置。当流量并发数大于100直接拒绝。

配置内容说明可参考https://github.com/alibaba/sentinel-golang/wiki/流量控制

[
    {
        "resource": "some-test",
        "metricType": 1,
        "count": 100.0,
        "controlBehavior":0
    }
]

创建完成后,在nacos配置列表中可以看到对应的限流配置。

2.png

4.3 Nacos数据源集成

4.3.1 创建项目

  1. 版本

    1. sentinel-golang 版本使用0.6.0,nacos-sdk-go 使用1.0.0
  2. go.mod
module sentinel-go-nacos-example

go 1.13

require (
    github.com/alibaba/sentinel-golang v0.6.0
    github.com/nacos-group/nacos-sdk-go v1.0.0
)
  1. main.go
package main

import (
    "fmt"
    "math/rand"
    "sync/atomic"
    "time"

    sentinel "github.com/alibaba/sentinel-golang/api"
    "github.com/alibaba/sentinel-golang/core/base"
    "github.com/alibaba/sentinel-golang/ext/datasource/nacos"
    "github.com/alibaba/sentinel-golang/util"
    "github.com/nacos-group/nacos-sdk-go/clients"

    "github.com/alibaba/sentinel-golang/ext/datasource"
    "github.com/nacos-group/nacos-sdk-go/common/constant"
)

type Counter struct {
    pass  *int64
    block *int64
    total *int64
}

func main() {
    //流量计数器,为了流控打印日志更直观,和集成nacos数据源无关。
    counter := Counter{pass: new(int64), block: new(int64), total: new(int64)}

    //nacos server地址
    sc := []constant.ServerConfig{
        {
            ContextPath: "/nacos",
            Port:        8848,
            IpAddr:      "127.0.0.1",
        },
    }
    //nacos client 相关参数配置,具体配置可参考https://github.com/nacos-group/nacos-sdk-go
    cc := constant.ClientConfig{
        TimeoutMs: 5000,
    }
    //生成nacos config client(配置中心客户端)
    client, err := clients.CreateConfigClient(map[string]interface{}{
        "serverConfigs": sc,
        "clientConfig":  cc,
    })
    if err != nil {
        fmt.Printf("Fail to create client, err: %+v", err)
        return
    }
    //注册流控规则Handler
    h := datasource.NewFlowRulesHandler(datasource.FlowRuleJsonArrayParser)
    //创建NacosDataSource数据源
    //sentinel-go 对应在nacos中创建配置文件的group
    //flow 对应在nacos中创建配置文件的dataId
    nds, err := nacos.NewNacosDataSource(client, "sentinel-go", "flow", h)
    if err != nil {
        fmt.Printf("Fail to create nacos data source client, err: %+v", err)
        return
    }
    //nacos数据源初始化
    err = nds.Initialize()
    if err != nil {
        fmt.Printf("Fail to initialize nacos data source client, err: %+v", err)
        return
    }
    //启动统计
    go timerTask(&counter)

    //模拟流量
    ch := make(chan struct{})
    for i := 0; i < 10; i++ {
        go func() {
            for {
                atomic.AddInt64(counter.total, 1)
                //some-test 对应在nacos 流控配置文件中的resource
                e, b := sentinel.Entry("some-test", sentinel.WithTrafficType(base.Inbound))
                if b != nil {
                    atomic.AddInt64(counter.block, 1)
                    // Blocked. We could get the block reason from the BlockError.
                    time.Sleep(time.Duration(rand.Uint64()%10) * time.Millisecond)
                } else {
                    atomic.AddInt64(counter.pass, 1)
                    time.Sleep(time.Duration(rand.Uint64()%10) * time.Millisecond)

                    // Be sure the entry is exited finally.
                    e.Exit()
                }

            }
        }()
    }
    <-ch
}

//statistic print
func timerTask(counter *Counter) {
    fmt.Println("begin to statistic!!!")
    var (
        oldTotal, oldPass, oldBlock int64
    )
    for {
        time.Sleep(1 * time.Second)
        globalTotal := atomic.LoadInt64(counter.total)
        oneSecondTotal := globalTotal - oldTotal
        oldTotal = globalTotal

        globalPass := atomic.LoadInt64(counter.pass)
        oneSecondPass := globalPass - oldPass
        oldPass = globalPass

        globalBlock := atomic.LoadInt64(counter.block)
        oneSecondBlock := globalBlock - oldBlock
        oldBlock = globalBlock
        fmt.Println(util.CurrentTimeMillis()/1000, "total:", oneSecondTotal, " pass:", oneSecondPass, " block:", oneSecondBlock)
    }
}

4.3.2 运行结果

3.png

4.3.3 动态更新限流配置

在项目启动过程中,在nacos中修改流控配置参数。将count 从100->400

4.png

可以看到打印了重新loadRule的日志,流量控制动态的由100->400

5.png

总结

在sentinel-go中使用nacos作为外部动态数据源,只需要将原来声明Rule以及加载Rule的部分 变成从nacos数据源读取。

在本文中只介绍了流量控制的集成,熔断,warmup,热点参数的集成也是相同的,只要按需修改配置的内容即可

配置内容参考地址:https://github.com/alibaba/sentinel-golang/wiki

关键代码:

    h := datasource.NewFlowRulesHandler(datasource.FlowRulesJsonConverter)
    nds, err := nacos.NewNacosDataSource(client, "sentinel-go", "flow", h)
    if err != nil {
        fmt.Printf("Fail to create nacos data source client, err: %+v", err)
        return
    }
    err = nds.Initialize()
    if err != nil {
        fmt.Printf("Fail to initialize nacos data source client, err: %+v", err)
        return
    }

相关链接

作者简介

张斌斌 Github 账号:sanxun0325,Nacos Commiter,Sentinel-Golang Contributor,现任职 OpenJaw 微服务团队。目前主要负责 Nacos、Sentinel-Golang 社区相关项目的开发工作,以及 Nacos 在 Golang 微服务生态中的推广集成工作。

阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”
查看原文

赞 1 收藏 0 评论 0

禹鼎侯 赞了文章 · 2020-09-22

Nacos Go 微服务生态系列(一)| Dubbo-go 云原生核心引擎探索

头图.png

作者 | 李志鹏

近几年,随着 Go 语言社区逐渐发展和壮大,越来越多的公司开始尝试采用 Go 搭建微服务体系,也涌现了一批 Go 的微服务框架,如 go-micro、go-kit、Dubbo-go 等,跟微服务治理相关的组件也逐渐开始在 Go 生态发力,如 Sentinel、Hystrix 等都推出了 Go 语言版本,而作为微服务框架的核心引擎--注册中心,也是必不可缺少的组件,市面已经有多款注册中心支持 Go 语言,应该如何选择呢?我们可以对目前主流的支持 Go 语言的注册中心做个对比。

1.png
图 1

根据上表的对比我们可以从以下几个维度得出结论:

  • 生态:各注册中心对 Go 语言都有支持,但是 Nacos、 Consul、Etcd 社区活跃,zookeeper 和 Eureka 社区活跃度较低;
  • 易用性:Nacos、Eureka、Consul 都有现成的管控平台,Etcd、zookeeper 本身作为 kv 存储,没有相应的管控平台,Nacos 支持中文界面,比较符合国人使用习惯;
  • 场景支持:CP 模型主要针对强一致场景,如金融类,AP 模型适用于高可用场景,Nacos 可以同时满足两种场景,Eureka 主要满足高可用场景,Consul、Zookeepr、Etcd 主要满足强一致场景,此外 Nacos 支持从其它注册中心同步数据,方便用户注册中心迁移;
  • 功能完整性:所有注册中心都支持健康检查,Nacos、Consul 支持的检查方式较多,满足不同应用场景,Zookeeper 通过 keep alive 方式,能实时感知实例变化;Nacos、Consul 和 Eureka 都支持负载均衡策略,Nacos 通过 Metadata selector 支持更灵活的策略;此外,Nacos、Eureka 都支持雪崩保护,避免因为过多的实例不健康对健康的实例造成雪崩效应。

综合上面各维度的对比,可以了解到 Nacos 作为注册中心有一定的优势,那么它对 Go 微服务生态的集成做得如何?为此,我们策划了本系列文章,该系列将为大家介绍 Nacos 在 Go 微服务生态集成中做的一些工作和实践经验,系列内容将主要包含以下三个篇章:

  • Dubbo-go 云原生核心引擎探索;
  • Sentinel-go 外部动态数据源初探;
  • go-micro 集成 Nacos 实践;

接下来我们首先探索下 Nacos 是如何与 Dubbo-go 集成。

引言

Dubbo-go 目前是 Dubbo 多语言生态中最火热的一个项目,从 2016 年发布至今,已经走过 5 个年头。最近,Dubbo-go 发布了 v1.5 版本,全面兼容 Dubbo 2.7.x 版本,支持了应用维度的服务注册与发现,和主流的注册模型保持一致,标志着 Dubbo-go 向云原生迈出了关键的一步。

作为驱动服务运转的核心引擎--注册中心,在切换到应用维度的注册模型后,也需要做相应的适配,本文将解析如何以 Nacos 为核心引擎实现应用维度的服务注册与发现,并且给出相应的实践案例。此外,本文代码基于 Dubbo-go v1.5.1,Nacos-SDK-go v1.0.0 和 Nacos v1.3.2。

服务注册与发现架构

从架构中,我们可以看到,与接口级别的服务注册发现不同的是,Dubbo-go 的 provider 启动后会调用 Nacos-go-sdk 的 RegisterInstance 接口向 Nacos 注册服务实例,注册的服务名即为应用名称,而不是接口名称。Conusmer 启动后则会调用 Subscribe 接口订阅该应用的服务实例变化,并对的实例发起服务调用。

2.png
图 2

服务模型

图 3 是我们 Dubbo-go 的应用维度服务发现模型,主要有服务和实例两个层级关系,服务实例的属性主要包含实例Id、主机地址、服务端口、激活状态和元数据。图 4 为 Nacos 的服务分级存储模型,包含服务、集群和实例三个层次。两者对比,多了一个集群维度的层级,而且实例属性信息能够完全匹配。

所以在 Dubbo-go 将应用服务实例注册到 Nacos 时,我们只需要将集群设置为默认集群,再填充服务和实例的相关属性,即可完成服务模型上的匹配。此外 Nacos 可以将服务注册到不同的 Namespace 下,实现多租户的隔离。

3.png
图 3

4.png
图 4

服务实例心跳维持

Dubbo-go 的 Provider 在向 Nacos 注册应用服务实例信息后,需要主动上报心跳,让 Nacos 服务端感知实例的存活与否,以判断是否将该节点从实例列表中移除。维护心跳的工作是在 Nacos-SDK-go 完成的,从图 5 代码中可以看到,当 Dubbo-go 调用 RegisterInstance 注册一个服务实例时,SDK 除了调用 Nacos 的 Register API 之外,还会调用 AddBeatInfo,将服务实例信息添加到本地缓存,通过后台协程定期向 Nacos 发送服务实例信息,保持心跳。

当服务下线时,可以通过调用 DeRegisterInstance 执行反注册,并移除本地的心跳保持任务,Nacos 实例列表中也会将该实例移除。

5.png
图 5

订阅服务实例变化

Dubbo-go 的 Consumer 在启动的时候会调用 Nacos-SDK-go 的 Subscribe 接口,该接口入参如图 6,订阅的时候只需要传递 ServiceName 即应用名和回调函数 SubscribeCallback,Nacos 在服务实例发生变化的时候即可通过回调函数通知 Dubbo-go。Nacos-SDK-go 是如何感知 Nacos 的服务实例变化的呢?主要有两种方式:

  • Nacos 服务端主动推送,Nacos-SDK-go 在启动的时候会监听一个 UDP 端口,该端口在调用 Nacos Register API 的时候作为参数传递,Nacos 会记录 Ip 和端口,当服务实例发生变化时,Nacos 会对所有监听该服务的 Ip 和端口发送 UDP 请求,推送变化后的服务实例信息;
  • Nacos-SDK-go 定期查询,SDK 会对订阅的服务实例定时调用查询接口,如果查询有变化则通过回调接口通知 Dubbo-go。作为兜底策略保证 Nacos 服务端推送失败后,仍能感知到变化。

6.png
图 6

此外 Nacos-SDK-go 还支持推空保护,当 Nacos 推送的实例列表为空时,不更新本地缓存,也不通知 Dubbo-go 变更,避免 Consumer 无可用实例调用,造成故障。同时,SDK 还支持服务实例信息本地持久化存储,可以保证在 Nacos 服务故障过程中,Consumer 重启也能获取到可用实例,具备容灾效果。

范例实践

1. 环境准备

7.png
图 7

2. Server 端搭建

进入 registry/servicediscovery/nacos/go-server/profiles 文件,可以看到有 dev、release 和 test 三个文件夹,分别对应开发、测试和生产配置。我们使用 dev 配置来搭建开发环境,dev 文件下有 log.yml 和 server.yml 文件,下面对 server.yml 配置进行修改。

remote 配置,这里使用公共的 Nacos 服务,address 支持配置多个地址,用逗号分割。params 参数配置 nacos-sdk 的日志目录。

remote:
  nacos:
    address: "console.nacos.io:80"
    timeout: "5s"
    params:
        logDir: "/data/nacos-sdk/log"

configCenter 配置:

config_center:
  protocol: "nacos"
  address: "console.nacos.io:80"

配置 server 端环境变量:

export CONF_PROVIDER_FILE_PATH=server端的server.yml文件路径
export APP_LOG_CONF_FILE=server端的log.yml文件路径

进入 registry/servicediscovery/nacos/go-server/app,运行 server.go 的 main 方法,可以从 Nacos 的控制台看到,应用 user-info-server 已经注册成功。

Nacos 的控制台地址:http://console.nacos.io/nacos/#/serviceManagement?dataId=&group=&appName=&namespace=

8.png
图 8

9.png
图 9

3. Client 端搭建

client 的配置文件在 registry/servicediscovery/nacos/go-server/profiles 目录下,需要修改的地方跟 server 端一样,这里不赘述。

配置 client 端环境变量:

export CONF_CONSUMER_FILE_PATH=client端的server.yml文件路径
export APP_LOG_CONF_FILE=client端的log.yml文件路径

进入 registry/servicediscovery/nacos/go-client/app,运行 client.go 的 main 方法,看到如下日志输出,表示调用 server 端成功。

10.png
图 10

相关链接

招聘信息

如果你对我们在做的事情感兴趣,欢迎你加入我们团队。内推邮箱:water.lyl@alibaba-inc.com

作者简介

李志鹏,Github 账号:Lzp0412,开源社区爱好者,Nacos Committer,Nacos-SDK-go 作者,现就职于阿里云云原生应用平台,主要参与服务发现、CoreDNS、ServiceMesh 相关工作,负责推动 Nacos Go 微服务生态建设。

Spring Cloud Alibaba 七天训练营

服务注册与发现是微服务架构体系中最关键的组件之一,为了带领大家系统入门微服务架构,9 月 24 日,由 Spring Cloud Alibaba 创始团队主笔的 Spring Cloud Alibaba 实战训练营将正式开营。七天时间了解微服务各模块的实现原理,手把手教学如何独立开发一个微服务应用,助力小白开发者从 0 到 1 建立系统化的知识体系。点击链接即可参与:https://developer.aliyun.com/learning/trainingcamp/spring/1

阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”
查看原文

赞 2 收藏 0 评论 1

禹鼎侯 关注了专栏 · 2020-09-22

阿里巴巴云原生

关注云原生技术趋势,输出最优质云原生内容

关注 59

认证与成就

  • 获得 25 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-07-24
个人主页被 1.4k 人浏览