0%

技术杂谈

最近服务器都迁移到腾讯云 TKE 了,就是腾讯云 Kubernetes 服务,然后最近有个需求是获取客户端真实 IP。

我的情况:

  • 服务用 Django 写的,通过 HTTP 请求头获取真实 IP
  • Django 通过 uwsgi 运行,并通过 Nginx 转发出去
  • Ingress 使用的 Nginx Ingress,而不是 TKE Ingress

下面说下几个关键配置:

Django 中获取 IP

获取方式如下:

1
2
3
4
5
6
7
def get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip

uwsgi 配置

uwsgi.ini 配置如下:

1
2
3
4
5
6
7
8
[uwsgi]
module = core.wsgi
master = true
processes = 1
vacuum = true
static-map = /static=/app/app/static
http = 127.0.0.1:8000
log-master = true

Nginx 配置

Nginx 需要转发 uwsgi 出去,并且加上三个关键请求头:

1
2
3
4
5
6
7
8
9
10
11
server {
listen 7777;
server_name localhost;

location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_pass http://127.0.0.1:8000;
}
}

Supervisor 配置

Supervisor 需要启动 Uwsgi 和 Nginx:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[supervisord]
nodaemon=true

[program:uwsgi]
command=uwsgi --ini /app/uwsgi.ini
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

[program:nginx]
command=/usr/sbin/nginx -g "pid /run/nginx/nginx.pid; daemon off;"
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

Dockerfile 配置

Dockkerfile 里面指定启动的命令为 supervisor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FROM python:3.7-alpine
ENV PYTHONUNBUFFERED 1
WORKDIR /app
COPY requirements.txt .
RUN apk update && apk add \
libuuid \
gcc \
nginx \
supervisor \
libc-dev \
linux-headers \
postgresql-libs \
postgresql-dev \
&& pip install pip -U \
&& pip install -r requirements.txt
COPY . /app
RUN mkdir -p /run/nginx
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY supervisord.conf /etc/supervisord.conf
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]

TKE 配置

这里参考来源为:https://cloud.tencent.com/document/product/457/48949

最关键的就是:

这里我需要修改 Service 的负载均衡:

修改 externalTrafficPolicy 为 Local 即可:

大功告成,后面 IP 就能成功获取到了。

技术杂谈

之前配置 uwsgi 的时候,配完了之后 Python 的日志就输出不出来了,其实可以在 uwsgi.ini 里面加一个日志配置就行了,很简单:

1
log-master = true

示例配置如下:

1
2
3
4
5
6
7
8
[uwsgi]
module = core.wsgi
master = true
processes = 1
vacuum = true
static-map = /static=/app/app/static
http = 127.0.0.1:8000
log-master = true

加上这句之后,日志就可以顺利输出到控制台了。

说到加湿器,那就不得不先提一下空气净化器。

之前其实我家里是没有空气净化器的,最近刚搬新住处就入手了一个,因为我想打造一套全新的小米之家,哈哈哈。

这个空气净化器用和没用给我的感觉区别不大,因为我确实感觉不到室内空气好一点或者坏一点有啥区别。但是记得在我刚打开空气净化器的一瞬间,上面的空气质量指数显示为 180 多(实际上这个是说的 PM 2.5 的指数),重度污染!我直接好家伙,难道我没用空气净化器的二十多年一直都呼吸的是重度污染的空气吗?心中不由倒吸一口凉气。净化器开了一会,那个质量指数慢慢降低,最后一直降到 10 以下,我心里也慢慢舒服多了,这时候我就感觉自己处在一个洁净的环境里面。所以,虽然我自己并不能感受出来空气变好了,但是心里是舒服的、安心的。

那为什么又扯到加湿器呢?

这是因为当我在开着空气净化器的时候,和往常一样打开我的加湿器…这时候,空气净化器显示的空气质量指数直接爆表了,直接 200 以上,当场就把我吓坏了。我当时就跟我老婆说,老婆我们这个加湿器有问题啊,它加湿后的水雾怎么控制质量这么差,肯定对身体有害。说完了之后我就去搜新的加湿器去了。因为最近我在搞米家智能家居,所以第一个搜的就是小米的加湿器,搜到这么一款:

OK,除菌加湿器,很好,有了这个除菌功能,加湿的空气应该不会那么差了。当时我那款「美的」的加湿器是没有这个功能的,所以我就又跟老婆说,老婆我搜到这个小米的加湿器是有除菌功能的,我买了这个肯定就没事了,于是我成功说服了我老婆,于是我的米家智能家居又新增了一员。

然而,等到货之后我灌上水重新打开,情况居然跟我想的不一样!空气净化器依然非常高,直逼 200,这是怎么回事?难道这个除菌功能一点用都没有?

抱着好奇又震惊的心情,我就去网上查,原来,这个和水有关系。空气指数上升的原因可以简单这么解释:这么因为我用的是自来水,而自来水含有氯、重金属等杂质,我买的加湿器又属于超声波式加湿器,它的原理是利用超声波将水打散成直径只有 1-5 微米的细小颗粒,然后利用风动装置将这些小颗粒吹到空气中,所以自来水中的杂质也就跟着进入空气中了,这就导致了空气净化器指数急剧上升,污染空气,长期这样下去对人体并不好。

看到这里,那就牵扯到了几个概念,一个是水,一个是加湿器,这两者的不同组合可能产生不同的结果,下面且听我一一道来。

首先说水啊,其实我们平常见到的水有很多种,名字也不同,目前常见的水可以分为这么几种类别:自来水、白开水、纯净水、矿泉水、天然水、蒸馏水,咋这么多?听着都懵啊,下面来解释下。

自来水

这个大家都知道了,水龙头里面直接放出来的水就是自来水,它是经过自来水厂加工、消毒之后生产出来的复合国家饮用水标准的工人们生产生活用的水,消毒的时候,里面含有一些微生物(如大肠杆菌)、重金属、有机污染物、泥沙等物质,另外由于自来水厂喜欢用氯气消毒,所以自来水里面还含有一些氯离子,总之这些都是对人体健康不利的。看到这里不要害怕,那就不能喝了吗?没关系,把水烧开了就行了。

白开水

白开水就是把自然水直接烧开得到的水,因为刚才也说了,自来水里面含有各种对人体有害的物质,把水烧开后,大部分细菌病毒都会被杀死,一些氯也会被分解掉,另外钙、镁和一些重金属离子也会在烧开的过程中形成沉淀,也就是我们通常说的水垢。不过金属离子是不能完全沉淀的,所以水中还会有溶解有少量的重金属离子,所以并不是完完全全是干净的,但基本问题不大。另外白开水其实口感非常不好,喝起来很涩,反正我是完全喝不惯白开水的味道。

矿泉水

大家肯定听说过矿泉水,但你平时喝的还真不一定是矿泉水,目前比较常见的矿泉水有百岁山、恒大冰泉等,它是是天然含有矿物质离子或盐或硫化物或二氧化碳气体等物质的水,矿泉水对水源地要求很高,不同水源地的矿物质含量也不同,比如百岁山的河寨山水源说是富含了偏硅酸锂、锶、钙、钾等多种有益于人体健康的微量元素,是为数不多的优质水源,当然依云也是。

纯净水

纯净水就是水里不含任何其他物质,只有水分子的水,没有任何矿物质。它是经过一些离子交换、反渗透、蒸馏等工艺制成的,过滤掉了细菌、病毒等有害物质,而且几乎不含任何矿物质,水的硬度也很低,用它烧水也不会形成水垢。纯净水可以有两种获取方式,一种就是家里装净水器,出来的都是纯净水。另外就是直接购买,比如怡宝就是纯净水。

天然水

天然水,最常见的就是农夫山泉,它的标识叫做饮用天然水,为啥他不标识天然矿泉水呢?它其实和矿泉水几乎差不多,但是其中有益的元素含量并不能达到天然矿泉水的要求。百岁山的水源相对农夫山泉更好一些,农夫山泉的水源有好几个基地,一般来说千岛湖的最好喝,带有一点甘甜的味道。不过不仔细对比,这个和矿泉水大查不差,反正都含有一些钙、镁、钾等矿物质。

蒸馏水

蒸馏水相比大家在学习化学等时候听说过,其实就是把水烧开,把水蒸气收集下来,然后再把水蒸气液化,分离出来的水就是纯水了,和纯净水一样也是不含矿物质的。由于工艺复杂,制造设备昂贵,所以蒸馏水一般在实验室比较常用,家庭基本上没有喝的,当然要买也能买到,比如屈臣氏的瓶装蒸馏水,价格不菲。

好,说完了以上的水,那哪种水适合放在加湿器(目前指的是超声波加湿器)里面不会引起空气指数升高呢?答案是纯净水和蒸馏水,其他的都不行。纯净水和蒸馏水加上之后,我的空气净化器指数基本没有变化,保持超绿色状态,舒服了。

另外平时喝的话,矿泉水、纯净水、天然水、白开水其实都行,虽然说矿泉水相比纯净水多了一些对人体有益的矿物质,但人体矿物质的摄入也不一定是靠水嘛,所以基本没有什么影响。

好,说完了水,那就得再说说加湿器了,这个其实也是有讲究的。

加湿器

市面上的加湿器主要分为两种,一种是超声波式加湿器,一种是冷蒸发式加湿器。

超声波式加湿器

大部分人家里的加湿器可能都是一二三百块钱的吧,加湿的过程中你还会看到白雾对不对?没错,这种加湿器就是超声波式加湿器,这种加湿器刚才也说了,利用高效的超声波振动将水打散成直径只有1-5微米的细小颗粒,再利用风动装置,将这些小颗粒吹到空气中,这种加湿器具有加湿速度快耗电量小、使用寿命长的优点,而且价格相对比较便宜,但存在不足之处在于对水要求高。

冷蒸发式加湿器

这种加湿器和超声波最大的区别就是加湿过程看不见水雾,它的原理就是通过循环风将液态水变成气态水,其实这种加湿器的加湿效果和超声波式相比并不占优势,不过加湿过程中,水是真正变成水蒸气而进入空气中的,所以水中的一些物质并不会进入到空气中,所以这种加湿器我们可以放心使用自来水,而不会引起空气净化器指数上升。不过这种加湿器价格都挺贵的,比超声波式加湿器贵了两三倍不止。

所以呢,到这里,基本就通透了吧,我用的加湿器就是第一种,这种加湿器就必须得用纯净水或者蒸馏水才能保证空气净化器不会爆表。如果加湿器是第二种,那随便用什么水都没啥关系了。

另外友情提醒,如果你的加湿器是第一种,请不要再用自来水了,对身体健康不利,还是改用纯净水吧,或者直接换个蒸发式加湿器也可以的。

所以说,这一番下来,学到还真不少,水和加湿器原来还有这么多讲究,生活真是处处皆学问啊。

参考

想知道农夫山泉 景田百岁山和怡宝味道的差别? - 徐野的回答 - 知乎 https://www.zhihu.com/question/29367546/answer/109448432

白开水、纯净水、矿泉水和蒸馏水哪个最适合做长期饮用水? - 安吉尔的回答 - 知乎 https://www.zhihu.com/question/20418550/answer/1271349511

你的加湿器到底该加什么水?今天让你秒懂!http://jd.zol.com.cn/734/7346674.html

怎样选购加湿器? - 老爸评测的回答 - 知乎 https://www.zhihu.com/question/22406803/answer/555850897

TypeScript

上一节学习了 TypeScript 的基本类型,本节再来学习下接口 Interfaces 的使用。

TypeScript 的一个重要的特性就是帮助检查参数的字段是否是合法的,比如检查某个参数包含字段 foo,且类型需要是 number 类型,否则就会报错。通过接口,即 Interface 我们可以方便地实现这个操作。

第一个 Interface

最简单的实现方式参考下面的例子:

1
2
3
4
5
6
function printLabel(labeledObj: { label: string }) {
console.log(labeledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

在这里我们声明了一个方法,叫做 printLabel,它接收一个参数叫做 labeledObj,在 labeledObj 后面声明了该参数需要的字段和类型,这里它需要一个字段 label,且类型必须要是 string 类型。在调用时,我们传入了一个 Object,它包含了两个字段,一个是 size,类型为 number,另一个是 label,类型为 string。这里值得注意的是传入的参数是比声明的参数多了一个字段 size 的,但是这并没有关系,编译器只检查传入的参数是否至少存在所需要的属性,对于多余的属性是不关心的。

运行结果如下:

1
[LOG]: "Size 10 Object"

如果此时我们将 label 属性的类型修改为 number:

1
2
3
function printLabel(labeledObj: { label: number }) {
console.log(labeledObj.label);
}

则会出现如下报错:

1
Argument of type '{ size: number; label: string; }' is not assignable to parameter of type '{ label: number; }'. Types of property 'label' are incompatible. Type 'string' is not assignable to type 'number'.

这里就提示 label 属性只能传入 number 类型,而不能是 string 类型。

但上面这个写法其实很不友好,如果属性比较多,那这个声明会非常复杂,而且不同方法如果都用到这个参数,难道还把它的声明都重复声明一遍?这也太不好了吧。

所以,为了更方便地实现声明,这里我们可以使用 Interface 来实现,上面的例子就可以改写为如下形式:

1
2
3
4
5
6
7
8
9
10
interface LabeledValue {
label: string;
}

function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

这里我们使用 interface 声明了一个类型声明,这样在 printLabel 就可以直接使用 Interface 的名称了。

怎么样?这种写法是不是感觉好多了。

Optional properties

某些情况下,某些字段并不是完全必要的,比如看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
interface LabeledValue {
label: string;
count: number,
message: string,
}

function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}

let myObj = { size: 10, count: 1, label: "Size 10 Object"};
printLabel(myObj);

其中 message 字段其实在 myObj 对象里面没有,而且这个字段也并不是必需的,但是该字段如果存在的话,必须是 string 类型。那像上面的写法,其实就会报错了:

1
Argument of type '{ size: number; count: number; label: string; }' is not assignable to parameter of type 'LabeledValue'. Property 'message' is missing in type '{ size: number; count: number; label: string; }' but required in type 'LabeledValue'.

这里说 message 字段没有传。

这时候我们可以将 message 标识为可选字段,只需要在字段后面加个 ? 就好了,写法如下:

1
2
3
4
5
interface LabeledValue {
label: string;
count: number,
message?: string,
}

这样就不会再报错了。

Readonly properties

在某些情况下,我们期望一个 Object 的某些字段不能后续被修改,只能在创建的时候声明,这个怎么做到呢?很简单,将其设置为只读字段就好了,示例如下:

1
2
3
4
5
6
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

这里我们声明了一个名为 Point 的 Interface,然后在创建 Point 的时候将 x 设置为 10。但后续如果我们想设置 x 的属性为 5 的时候,就会报错了:

1
Cannot assign to 'x' because it is a read-only property.

这样就可以保证某些字段不能在后续操作流程中被修改,保证了安全性。

另外我们可以使用 ReadonlyArray 来声明不可变的 Array,一旦初始化完成之后,后续所有关于 Array 的操作都会报错,示例如下:

1
2
3
4
5
6
7
8
9
10
11
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;

ro[0] = 12; // error!
Index signature in type 'readonly number[]' only permits reading.
ro.push(5); // error!
Property 'push' does not exist on type 'readonly number[]'.
ro.length = 100; // error!
Cannot assign to 'length' because it is a read-only property.
a = ro; // error!
The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.

另外到这里大家可能疑惑 readonly 和 const 是什么区别,二者不都代表不可修改吗?其实区分很简单,readonly 是用来修改 Object 的某个属性的,而 const 是用来修饰某个变量的。

Function Types

除了用 interface 声明 Object 的字段,我们还可以声明方法的一些规范,示例如下:

1
2
3
interface SearchFunc {
(source: string, subString: string): boolean;
}

这里就是用 interface 声明了一个 Function,前半部分是接收的参数类型,后面 boolean 是返回值类型。

声明 interface 之后,我们便可以声明一个 Function 了,写法如下:

1
2
3
4
5
6
7
8
interface SearchFunc {
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc = (source: string, subString: string) => {
let result = source.search(subString);
return result > -1;
};

这里声明了一个 Function 叫做 mySearch,其中其参数和返回值严格按照 SearchFunc 这个 Interface 来实现,那就没问题。

如果我们将返回值改掉,改成非 boolean 类型,示例如下:

1
2
3
4
5
6
7
8
interface SearchFunc {
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc = (source: string, subString: string) => {
let result = source.search(subString);
return result;
};

这时候就会得到如下报错:

1
Type '(source: string, subString: string) => number' is not assignable to type 'SearchFunc'. Type 'number' is not assignable to type 'boolean'.

这里就说返回值是 number,而不是 boolean。

Class Types

除了声明 Function,interface 还可以用来声明 Class,主要作用就是声明 class 里面所必须的属性和方法,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}

class Clock implements ClockInterface {
currentTime: Date = new Date();
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) {}
}

这个简直跟其他语言的接口定义太像了。定义好了 ClockInterface 之后,class 需要使用 implements 来实现这个接口,同时必须要声明 currentTime 这个变量和 setTime 方法,类型也需要完全一致,不然就会报错。

Extending Interfaces

另外 Interface 之间也是可以继承的,相当于在一个 Interface 上进行扩展,示例如下:

1
2
3
4
5
6
7
8
9
10
11
interface Shape {
color: string;
}

interface Square extends Shape {
sideLength: number;
}

let square = {} as Square;
square.color = "blue";
square.sideLength = 10;

这里 Shape 这个 Interface 只有 color 这个属性,而 Square 则继承了 Shape,并且加了 sideLength 属性,那其实现在 Square 这个接口声明了 color 和 sideLength 这两个属性。

另外 Interface 还支持多继承,获取被继承的 Interface 的所有声明,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Shape {
color: string;
}

interface PenStroke {
penWidth: number;
}

interface Square extends Shape, PenStroke {
sideLength: number;
}

let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

但如果同名的字段不一致怎么办呢?比如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Fruit {
color: string
}

interface Apple extends Fruit {
hasLeaf: boolean
}

interface Orange extends Fruit {
size: number,
hasLeaf: number
}

interface Watermalon extends Apple, Orange {

}

这里 hasLeaf 在 Apple 里面是 boolean 类型,在 Orange 里面是 number 类型,最后 Watermalon 继承了这两个 Interface 会怎样呢?

很明显,报错了,结果如下:

1
Interface 'Watermalon' cannot simultaneously extend types 'Apple' and 'Orange'. Named property 'hasLeaf' of types 'Apple' and 'Orange' are not identical.

意思就是说字段类型不一致。

所以,要多继承的话,需要被继承的 Interface 里面的属性不互相冲突,不然是无法同时继承的。

Interfaces Extending Classes

在某些情况下,Interface 可能需要继承 Class,Interface 扩展 Class 时,它将继承该 Class 的成员,但不继承其实现。这就类似该 Interface 声明了该类的所有成员而没有提供实现。

另外 Interface 甚至可以继承 Class 的私有成员和受保护成员。这意味着,当创建一个扩展带有私有或受保护成员的 Class 的 Interface 时,该 Interface 只能由该 Class 或其子 Class 实现。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Control {
private state: any;
}

interface SelectableControl extends Control {
select(): void;
}

class Button extends Control implements SelectableControl {
select() {}
}

class TextBox extends Control {
select() {}
}

class ImageControl implements SelectableControl {
// Error, Class 'ImageControl' incorrectly implements interface 'SelectableControl'.
// Types have separate declarations of a private property 'state'.
private state: any;
select() {}
}

上面我们可以知道,当创建一个扩展带有私有或受保护成员的 Class 的 Interface 时,该 Interface 只能由该 Class 或其子 Class 实现。在这里 ImageControl 由于没有继承 Control,但同时 Control 还包含了私有成员变量,所以 ImageControl 并不能继承得到 state 这个私有成员变量,所以会报错。

以上便是关于 Interface 的一些用法,后面会继续总结其他的用法,如 Functions、Classes 等详细用法。

TypeScript

之前的时候都是用 JavaScript 开发,但是最近发现随着项目的推进,一些项目越来越多地要求使用 TypeScript 来开发,所以不得不来专门学习一下 TypeScript 了,本篇文章就简单记录下学习 TypeScript 的过程。

介绍

先看看 TypeScript 的官方介绍:

Typed JavaScript at Any Scale.

TypeScript extends JavaScript by adding types. By understanding JavaScript, TypeScript saves you time catching errors and providing fixes before you run code. Any browser, any OS, anywhere JavaScript runs. Entirely Open Source.

我们知道 JavaScript 是弱类型的语言,而实际上说白了就是加上类型标识的 JavaScript 语言,加上类型标识之后,可以帮助我们减少开发的调试成本,尽早发现错误,节省更多时间。同时 TypeScript 是完全开源的,它编译后得到的 JavaScript 支持任何浏览器和操作系统运行。

截止本文撰写的时间,TypeScript 已经更新到 4.1 版本,官方文档地址:https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html。

相关资料

学习 TypeScript 当然要看原汁原味的资料,推荐 TypeScript 官方的 Handbook,链接为:https://www.typescriptlang.org/docs/handbook/intro.html。

本文所总结的内容基于 Handbook 的内容加上个人的一些修改而成。

另外本节的代码推荐大家直接在 TypeScript 提供的 Playgroud 里面运行即可,不用自己再初始化 Node 和 TypeScript 环境,简单方便,链接为:https://www.typescriptlang.org/play,大家直接打开就行了,预览如下:

image-20201228234110415

比如默认情况下就有两行初始化代码:

1
2
const anExampleVariable = "Hello World";
console.log(anExampleVariable);

点击 Run 之后就可以直接运行,控制台输出结果显示在右侧,内容如下:

1
[LOG]: "Hello World"

如果你运行成功了,那我们就可以开始接下来的学习了。

下面正式开始介绍。

TypeScript 主要就是在 JavaScript 基础上扩展了一些类型,所以这里就分各种类型来进行介绍。

基本类型

Boolean

最常见的基本类型就是布尔类型了,其值就是 true 或者 false,类型声明用 boolean 就好了,在 TypeScript 中,声明一个 boolean 类型的变量写法如下:

1
2
let isDone: boolean = false;
console.log(isDone, typeof isDone);

这里注意到,声明类型可以在变量名的后面加上一个冒号,然后跟一个类型声明,和 Python 的 Type Hint 非常像。然后我们用 console 的 log 方法输出了这个变量,并用 typeof 输出了它的类型。

运行结果如下:

1
[LOG]: false,  "boolean"

可以看到运行结果就被输出出来了,同时 typeof 就是这个变量的类型,结果是一个字符串,就是 boolean。

是不是很简单?基本套路就是在变量的后面跟一个冒号再跟一个类型声明就好了。

Number

Number 即数值类型,对于这个类型,TypeScript 和 JavaScript 是一样的,Number 可以代表整数、浮点数、大整数。其中大整数需要单独用 bigint 来表示。另外 Number 还可以代表十六进制、八进制、二进制。

示例如下:

1
2
3
4
5
6
7
8
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
let big: bigint = 100n;

console.log(hex, typeof hex);
console.log(big, typeof big);

其中 bigint 需要 ES2020 的支持,需要在 TS Config 里面设置下:

image-20201228235911831

运行结果如下:

1
2
[LOG]: 61453,  "number" 
[LOG]: 100, "bigint"

这里输出了 hex 和 big 变量的值和类型,其中 hex 本身是用十六进制声明的,打印输出的时候被转化为了十进制,同时其类型为 number。

另外对于 bigint 类型来说,值的后面需要加一个 n,即 100 和 100n 代表的类型是不一样的,后者的类型是 bigint,前者的类型是 number。

假如把 bigint 类型的变量声明为 number 类型是会报错的,比如这样就是错误的:

1
let big: number = 100n;

报错内容如下:

1
Type 'bigint' is not assignable to type 'number'.

String

String 即字符串类型,需要用 string 来声明类型。字符串可以用单引号或者双引号或者斜引号声明,其中斜引号就是模板字符串。

示例如下:

1
2
3
4
5
6
let color: string = "blue";
color = 'red';
let fullName: string = `Bob Bobbington`;
let age: number = 37;
let sentence: string = `Hello, my name is ${fullName}. I'll be ${age + 1} years old next month.`;
console.log(sentence, typeof sentence);

运行结果如下:

1
[LOG]: "Hello, my name is Bob Bobbington. I'll be 38 years old next month.",  "string"

嗯,没什么特殊的,就是字符串类型。

Array

Array 即数组,声明可以有两种方式,一种是 type[] 这样的形式,一种是 Generics 泛型的形式,示例如下:

1
2
3
4
5
6
let list: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3];
let list3: Array<any> = [1, "2", 3];
console.log(list, typeof list);
console.log(list2, typeof list2);
console.log(list3, typeof list3);

其中 list 就是使用了 type[] 这样的声明方式,声明为 number[],那么数组里面的每个元素都必须要是 number 类型,list2 则是使用了泛型类型的声明,和 list 的效果是一样的,另外 list3 使用了 any 泛型类型,所以其值可以不仅仅为 number,因此这里 list3 的第二个元素设置为了字符串 2。

运行结果如下:

1
2
3
[LOG]: [1, 2, 3],  "object" 
[LOG]: [1, 2, 3], "object"
[LOG]: [1, "2", 3], "object"

结果的 typeof 输出的结果可能让人意外,不应该是输出其声明的类型吗,怎么是 object?这是因为浏览器真正执行的是刚才的 TypeScript 编译生成的 JavaScript,而 JavaScript 本身的 Array 和 Object 等类型,typeof 都统一返回 object 类型,因此得到的结果就是 object 了。

Tuple

Tuple 即元组,它可以允许我们声明固定数量和顺序的 Array,来看个例子就懂了:

1
2
3
let x: [string, number];
x = [1, 2]; // Error
x = ["Hello", 2]; // Correct

比如这里声明了一个变量 x,x 必须是一个 Array,按照顺序来说,其第一个元素必须要是 string,第二个元素必须要是 number,所以第二行的声明就会报错,第三行的声明才是正确的。

有了类型声明之后,在编译阶段就能发现错误或者编辑器给出方法输入提示。比如:

1
2
console.log(x[0].substring(1)); // Correct
console.log(x[1].substring(1)); // Error, Property 'substring' does not exist on type 'number'.

比如这里第一行我们在敲 substring 的时候,编辑器根据类型判断出 x[0] 是 string 类型,那就可以给出 substring 的提示。而对于第二行代码的 x[1],编译器会直接检查出其中存在错误,这有助于我们在静态类型检查时候及时发现问题,减少 Bug 出现的概率。

Enum

Enum 即枚举类型,这个非常有用,有时候我们想定义的变量其实就只有某几种取值,那完全可以定义为枚举类型。

用法如下:

1
2
3
4
5
6
7
8
9
10
enum Stage {
Debug,
Info,
Warning,
Error,
Critical,
}

let stage: Stage = Stage.Critical;
console.log(stage, typeof stage);

这里我们声明了一个枚举类型,其名称叫做 Stage,值有五个。然后声明的变量直接用 Stage 修饰即可,值则可以直接取 Stage 的某个值,这里取值为 Stage.Critical

最后打印输出该变量,结果如下:

1
[LOG]: 4,  "number"

结果居然是 4,这个怎么情况?

原来是因为枚举类型,它会按照枚举值的声明顺序自动编号,比如 Debug 的值就是 0,Info 就是 1,以此类推。默认情况下是从 0 开始编号的,不过我们也可以手动更改编号的起始值,比如 Debug 从 1 开始编号,可以声明如下:

1
2
3
4
5
6
7
8
9
10
enum Stage {
Debug = 1,
Info,
Warning,
Error,
Critical,
}

let stage: Stage = Stage.Critical;
console.log(stage, typeof stage);

最后可以看到 Critical 的编号就变成 5 了,运行结果如下:

1
[LOG]: 5,  "number"

有点意思。

那既然它是按照顺序来赋值的,那我如果在前面的值设置大一点,会不会出现后面的值和前面的值相等的情况?比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Stage {
Debug = 1,
Info = 3,
Warning = 2,
Error,
Critical,
}

let stage: Stage = Stage.Error;
let stage2: Stage = Stage.Info;
console.log(stage, typeof stage);
console.log(stage2, typeof stage2);
console.log(stage == stage2);

看看这样会发生什么?这里 Error 按照常理来说会从 Warning 开始自增,值应该为 3,那 Info 我也设置为 3,二者会是相等吗?

运行结果如下:

1
2
3
[LOG]: 3,  "number" 
[LOG]: 3, "number"
[LOG]: true

果不其然,二者还都是 3,而且它们就是相等的。所以,对于枚举类型,我们一定要注意声明值的时候最好不要引起自增值和设定值之间的冲突,不然会引入不必要的麻烦。

另外对于枚举类型,我们还可以根据值进行查询,比如对于上述声明,我们不知道哪个值等于 2 或者 3,那可以直接将值传给 Stage 进行查询,得到的结果是一个字符串,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Stage {
Debug = 1,
Info = 3,
Warning = 2,
Error,
Critical,
}

let stage: string = Stage[3];
console.log(stage, typeof stage);

let stage2: string = Stage[2];
console.log(stage2, typeof stage2);

运行结果如下:

1
2
[LOG]: "Error",  "string" 
[LOG]: "Warning", "string"

比如这里 Stage[2] 就是查找枚举值等于 2 的枚举名称,结果是 Warning。那值相同的咋办呢?比如 3,看结果它返回的是 Error,看来 Error 把 Info 覆盖掉了,查询的结果是最新的一个枚举名称。注意这里返回的结果是字符串类型,不是枚举类型,它仅仅代表枚举的名称而已。

Unknown

Unknown 即未知类型,一般在类型不确定的情况下可以声明为 unknown 类型。比如说一个数据可能是 API 返回的,它可能是数值类型也可能是字符串类型,并不知道,这时候我们就可以将其声明为 unknown 类型。

示例如下:

1
2
3
4
5
let notSure: unknown = 4;
notSure = "maybe a string instead";

// OK, definitely a boolean
notSure = false;

比如这里 notSure 就声明了 unknown 类型,一开始它是数值类型,但当然也可以是字符串或者布尔类型。

那这样不就没啥用了吗?和 JavaScript 有啥不同吗?其实 unknown 还有其他用处,比如说编译器可以在一些判定条件下对 unknown 的值进行精确化处理,比如说一个 if 条件,判定了类型为布尔类型,那么其他和该类型相关的变量都会执行静态类型检查。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
declare const maybe: unknown;
// 'maybe' could be a string, object, boolean, undefined, or other types
const aNumber: number = maybe;
// Type 'unknown' is not assignable to type 'number'.

if (maybe === true) {
// TypeScript knows that maybe is a boolean now
const aBoolean: boolean = maybe;
// So, it cannot be a string
const aString: string = maybe;
// Type 'boolean' is not assignable to type 'string'.
}

if (typeof maybe === "string") {
// TypeScript knows that maybe is a string
const aString: string = maybe;
// So, it cannot be a boolean
const aBoolean: boolean = maybe;
// Type 'string' is not assignable to type 'boolean'.
}

这里使用 declare 声明了一个类型变量,然后通过类型变量里面的判定条件就能配合检查其他变量的类型了。

Any

和 Unknown 的情形类似,我们还可以使用 any 来代表任意的类型,示例如下:

1
2
3
declare function getValue(key: string): any;
// OK, return value of 'getValue' is not checked
const str: string = getValue("myString");

这里声明了一个方法,返回类型就是 any,这样的话返回类型就可以是任意的结果了。

那 any 和 unknown 有什么不同呢?any 的自由度会更高一点,如果声明为 any,那么静态类型检查都会通过,即使某个变量的属性不存在。示例如下:

1
2
3
4
5
6
7
8
9
let looselyTyped: any = 4;
// OK, ifItExists might exist at runtime
looselyTyped.ifItExists();
// OK, toFixed exists (but the compiler doesn't check)
looselyTyped.toFixed();

let strictlyTyped: unknown = 4;
strictlyTyped.toFixed();
// Error, Object is of type 'unknown'.

可以看到,前两行的静态类型检查是能过的,但是 unknown 就过不了。

所以,什么时候用 unknown 什么时候用 any 呢?可以从含义上来进行区分:如果某个变量我确实就不知道它的类型,即我没办法知道它的类型,那就用 unknown;如果某个值但其实我确实有办法知道可能的类型,但确实它的类型自由度比较高,那就可以被声明为 any。

但在不必要的情况下,尽量减少 any 类型的使用。

Void

Void 一般用于声明方法的返回值,如果一个方法不返回任何结果,那就用 Void,示例如下:

1
2
3
function warnUser(): void {
console.log("This is my warning message");
}

Null 和 Undefined

其实 TypeScript 还专门为 null 和 undefined 声明了类型,一般 null 就声明为 null 类型,undefined 就声明为 undefined 类型,不过这两个并不太常用,示例如下:

1
2
3
// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;

另外对于其他类型来说,其值也可以是 null 或者 undefined,比如 number 类型的变量,其值为 null 也是完全可以的。不过这里有个前提条件就是,TS Config 里面的 strictNullChecks 应该是关闭状态,不然会报错的。

设置如下:

image-20201229014015393

比如:

1
2
let a: number = undefined;
let b: string = null;

这样就是没问题的了。

Never

这个类型也比较特殊,它代表你永远得不到它的结果。比如一个方法始终抛出一个异常,或者处于一个无限循环中,那么就可以声明为 never,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Function returning never must not have a reachable end point
function error(message: string): never {
throw new Error(message);
}

// Inferred return type is never
function fail() {
return error("Something failed");
}

// Function returning never must not have a reachable end point
function infiniteLoop(): never {
while (true) {}
}

Object

Object 是 JavaScript 本身带有的类型,它表示非原始型的类型,即任何不是 number、string、boolean、bigint、symbol、null、undefined 的类型。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
declare function create(o: object | null): void;

// OK
create({ prop: 0 });
create(null);
create(42);
// Error, Argument of type '42' is not assignable to parameter of type 'object | null'.
create("string");
// Error, Argument of type '"string"' is not assignable to parameter of type 'object | null'.
create(false);
// Error, Argument of type 'false' is not assignable to parameter of type 'object | null'.
create(undefined);
// Error, Argument of type 'undefined' is not assignable to parameter of type 'object | null'.

不过,一般不用。

Type assertions

一般情况下,我们确确实实知道某个变量属于什么类型,或者在某种情况下确实需要这种类型的转化,则可以显式的声明某个类型,示例如下:

1
2
3
4
let someValue: unknown = "this is a string";
let strLength: number = (someValue as string).length;
let someValue2: unknown = "this is a string";
let strLength2: number = (<string>someValue2).length;

这里有两种使用方式,一种是 as,一种是尖括号声明。

注意

另外值得注意到是,以上的一些类型声明,使用大写形式 Number, String, Boolean, Symbol and Object 也是可以的,不过不推荐这么做,推荐还是用小写的形式。

比如这样其实是可以的:

1
2
3
4
5
function reverse(s: String): String {
return s.split("").reverse().join("");
}

reverse("hello world");

但更推荐使用小写,写成如下形式:

1
2
3
4
5
function reverse(s: string): string {
return s.split("").reverse().join("");
}

reverse("hello world");

总结

以上便是一些基本的类型声明方式,暂时先总结这么多,后面还会继续整理更高级的用法,比如 Function、Interface、Class 等。

技术杂谈

最近的服务都放到腾讯云上了,但是最近遇到了一个问题是 Docker 云磁盘满了,部署的适合提示 no space left 等等。

然后我就登录云主机看了下磁盘情况,好家伙:

1
2
3
4
5
6
7
8
9
ubuntu@VM-0-2-ubuntu:/var/lib/docker/containers$ df
Filesystem 1K-blocks Used Available Use% Mounted on
udev 3869584 0 3869584 0% /dev
tmpfs 777376 15124 762252 2% /run
/dev/vda1 103145380 9460508 89390616 10% /
tmpfs 3886864 24 3886840 1% /dev/shm
tmpfs 5120 0 5120 0% /run/lock
tmpfs 3886864 0 3886864 0% /sys/fs/cgroup
/dev/vdb 10255636 10155252 - 99% /var/lib/docker

vdb 这个磁盘就是我的数据盘,里面放了容器数据,现在已经到了 99% 了,一共就 10G。

然后我就去腾讯云控制台搞了下数据盘的扩容,然后直接扩容到了 200G。

但是回过头来看了下磁盘还是 10G,一变不变。

但 fdisk 已经看到变了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ubuntu@VM-0-2-ubuntu:/var/lib/docker/containers$ sudo fdisk -l
Disk /dev/vda: 100 GiB, 107374182400 bytes, 209715200 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x3fa1d255

Device Boot Start End Sectors Size Id Type
/dev/vda1 * 2048 209715166 209713119 100G 83 Linux


Disk /dev/vdb: 200 GiB, 214748364800 bytes, 419430400 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes

这是因为之前的数据被格式化为了 ext4,但是新扩容的部分并没有。

问了腾讯云的工程师,这时候必须要重新格式化才能用上新的容量,没有好的解决方法。

最后的解决方案:

  • 将当前节点所有 Pod 驱逐到其他的节点。
  • 当前节点退出集群然后配置磁盘,重新操作 mount 和格式化。(但是这里劝别折腾了,终究还是要格式化的。
  • 把节点再加回来,这时候可以选重新格式化数据盘,mount 到 /var/lib/docker。
  • 再把之前的 Pod 收回到本节点就好了。

完毕。

感觉还是不完善啊,像 Azure Kubernetes 的扩容就方便多了,不用整这么多麻烦事。

技术杂谈

就昨天的时候,Python 之父 Guido 发了个推特,说自己觉得退休太无聊了,然后于是乎决定加入微软。

同时推特上大神 Anders Hejlsberg(Delphi、C# 和 TypeScript 之父)转发了推特,表示希望可以一起工作。

要知道,去年那会去年 10 月 30 日,Python 之父 Guido 宣布退休,当时他发推文说,“这件事感觉既苦涩又甜蜜:苦涩的是,我马上要离开 Dropbox,现在已经退休;甜蜜的是,在 Dropbox 做工程师期间,我学到很多,比如类型标注就来自这段经历。我会非常怀念在这里工作的日子。”

看到这个消息我既震惊又激动,Python 之父加入我司了?那我岂不是和他也算做同事了?哈哈哈。

当时我看到这个消息之后,第一时间就去公司的系统里面查了下,果真就查到了,这里放个图。

他的 Title 叫 DISTINUISHED ENGINEER,这个 Title 可不是一般的牛逼,在微软,有这样的 Title 的人可是屈指可数的,DISTINUISHED 意思就是杰出的,非常牛逼的意思,能有这种 Title 的是为业界或公司做出过特别特别大贡献的,特别有影响力的,可能普通人在微软呆个二十多年都不一定能到这个地位。他的职级和 Report Line 就不说了,他距离微软 CEO 纳德拉只差 3 级,稍微形象点说就是对标阿里 P11 或者 P12 的位置吧。

他的部门就直接写了 Python and Tools for AI,和 AI 相关,同时 Guido 又这么热爱开源,微软也在一直拥抱开源,期待 Python 之父将来不久之后又出新的杰作吧。

要知道 Python 之父 Guido 已经 64 岁了,或许大佬就是大佬吧,退休了感觉无聊,就来微软来玩玩。怎么说呢?他这个地位就是类似达成一些自我追求和实现自己的一些价值,顺便解解闷,这种境界完全不是我们能比的了。反过来想想中国这个环境,多少人都是用代码来换取一份工作谋求生存,然后 35 岁干不动了可能就被辞退。本来想展开说说的,但是感觉说来都是痛,就不多说了,希望我将来不会为生活所迫,能够自由地继续追求自己热爱的东西。

另外本想说说 Guido 的履历的,但看到其他的一些文章已经整理得挺好了,我就不再重复了,大家想了解更多的话可以去看看 https://mp.weixin.qq.com/s/ZQJClbYiKP5cAnKjB_ZfUg。

总之,我就是来宣布这个和 Python 之父成为“同事”的开心的消息,值得纪念下,然后顺便期待下 Python 之父将来出新的杰作!

技术杂谈

本章将告诉你该如何去对request模块进行二次封装,暂时并不会告诉你HTTP协议及原理、URL等相关。当然你会使用然后在来阅读此文章一定会另有所获。我已经迫不及待要告诉你这个小秘密,以及想与你交流了。没时间解释了,快来一起和我一起探讨相关的内容吧

官方文档对requests的定义为:Requests 唯一的一个非转基因的 Python HTTP 库,人类可以安全享用。

使用Python写做爬虫的小伙伴一定使用过requests这个模块,初入爬虫的小伙伴也一定写过N个重复的requests,这是你的疑问。当然也一直伴随着我,最近在想对requests如何进行封装一下,让他支持支持通用的函数。若需要使用,直接调用即可。

那么问题来了,如果要写个供自己使用通用的请求函数将会有几个问题

  • requests的请求方式(GET\POST\INPUT等等)
  • 智能识别网站的编码,避免出现乱码
  • 支持文本、二进制(图片、视频等为二进制内容)
  • 以及还需要傻瓜一点,那就是网站的Ua(Ua:User-Agent,基本上网站都会验证接受到请求的Ua。来初步判断是爬虫还是用户)

那么咱们就针对以上问题开干吧

Requests的安装

在确保python环境搭建完成后直接使用pip或者conda命令进行安装,安装命令如下:

1
2
3
4
5
pip install requests
conda install requests

# 或者下载过慢点话,可以使用国内的pip镜像源,例如:
pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple/

安装完成后,效果图如下:

初探requests基本使用

HTTP 中最常见的请求之一就是 GET 请求,下面我们来详细了解利用 requests 库构建 GET 请求的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import requests

response = requests.get('http://httpbin.org/get')

# 响应状态码
print("response.status_code:", response.status_code)
# 响应头
print("response.headers:", response.headers)
# 响应请求头
print("response.request.headers:", response.request.headers)
# 响应二进制内容
print("response.content:", response.content)
# 响应文本
print("response.text", response.text)

# 返回如下
response.status_code: 200
response.headers: {'Date': 'Thu, 12 Nov 2020 13:38:05 GMT', 'Content-Type': 'application/json', 'Content-Length': '306', 'Connection': 'keep-alive', 'Server': 'gunicorn/19.9.0', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'true'}
response.request.headers: {'User-Agent': 'python-requests/2.24.0', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}
response.content: b'{\n "args": {}, \n "headers": {\n "Accept": "*/*", \n "Accept-Encoding": "gzip, deflate", \n "Host": "httpbin.org", \n "User-Agent": "python-requests/2.24.0", \n "X-Amzn-Trace-Id": "Root=1-5fad3abd-7516d60b3e951824687a50d8"\n }, \n "origin": "116.162.2.166", \n "url": "http://httpbin.org/get"\n}\n'
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.24.0",
"X-Amzn-Trace-Id": "Root=1-5fad3abd-7516d60b3e951824687a50d8"
},
"origin": "116.162.2.166",
"url": "http://httpbin.org/get"
}

requests基本使用已经经过简单的测试了,是否有一点点feel呢?接下来我们直接将它封装为一个函数以供随时调用

示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests

urls = 'http://httpbin.org/get'


def downloader(url, headers=None):
response = requests.get(url, headers=headers)
return response


print("downloader.status_code:", downloader(url=urls).status_code)
print("downloader.headers:", downloader(url=urls).headers)
print("downloader.request.headers:", downloader(url=urls).request.headers)
print("downloader.content:", downloader(url=urls).content)
print("downloader.text", downloader(url=urls).text)

# 返回效果如上所示,此处省略

以上我们就把,请求方法封装成了一个函数。将基本的url,headers以形参的方式暴露出来,我们只需传入需要请求的url即可发起请求,至此一个简单可复用的请求方法咱们就完成啦。

完~~~

以上照顾新手的就基本完成了,接下来我们搞点真家伙。

二次封装

请求函数的封装

由于请求方式并不一定(有可能是GET也有可能是POST),所以我们并不能智能的确定它是什么方式发送请求的。

Requests中request方法以及帮我们实现了这个方法。我们将他的请求方式暴露出来,写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
urls = 'http://httpbin.org/get'


def downloader(url, method=None, headers=None):
_method = "GET" if not method else method
response = requests.request(url, method=_method, headers=headers)
return response


print("downloader.status_code:", downloader(url=urls).status_code)
print("downloader.headers:", downloader(url=urls).headers)
print("downloader.request.headers:", downloader(url=urls).request.headers)
print("downloader.content:", downloader(url=urls).content)
print("downloader.text", downloader(url=urls).text)

由于大部分都是GET方法,所以我们定义了一个默认的请求方式。如果需要修改请求方式,只需在调用时传入相对应的方法即可。例如我们可以这样

1
downloader(urls, method="POST")

文本编码问题

解决由于request的误差判断而造成解码错误,而得到乱码。

此误差造成的原因是可能是响应头的Accept-Encoding,另一个是识别错误

1
2
# 查看响应编码
response.encoding

此时我们需要借用Python中C语言编写的cchardet这个包来识别响应文本的编码。安装它

1
2
pip install cchardet -i  https://pypi.tuna.tsinghua.edu.cn/simple/
# 如果pip直接安装失败的话,直接用清华源的镜像。
1
2
3
4
5
6
7
8
9
10
# 实现智能版的解码:如下
encoding = cchardet.detect(response.content)['encoding']



def downloader(url, method=None, headers=None):
_method = "GET" if not method else method
response = requests.request(url, method=_method, headers=headers)
encoding = cchardet.detect(response.content)['encoding']
return response.content.decode(encoding)

区分二进制与文本的解析

在下载图片、视频等需获取到其二进制内容。而下载网页文本需要进行encode。

同理,我们只需要将一个标志传进去,从而达到分辨的的效果。例如这样

1
2
3
4
5
def downloader(url, method=None, headers=None, binary=False):
_method = "GET" if not method else method
response = requests.request(url, method=_method, headers=headers)
encoding = cchardet.detect(response.content)['encoding']
return response.content if binary else response.content.decode(encoding)

默认Ua

在很多时候,我们拿ua又是复制。又是加引号构建key-value格式。这样有时候仅仅用requests做个测试。就搞的麻烦的很。而且请求过多了,直接就被封IP了。没有自己的ip代理,没有钱又时候还真有点感觉玩不起爬虫。

为了减少被封禁IP的概率什么的,我们添加个自己的Ua池。Ua池的原理很简单,内部就是采用随机的Ua,从而减少被发现的概率.至于为什么可以达到这这样的效果,在这里仅作简单介绍。详细可能要从计算机网络原理说起。

结论就是你一个公司里大多采用的都是同一个外网ip去访问目标网址。那么就意味着可能你们公司有N个人使用同一个ip去访问目标网址。而封禁做区分的一般由ip访问频率和浏览器的指纹和在一起的什么鬼东东。简单理解为Ua+ip访问频率达到峰值,你IP就对方关小黑屋了。

构建自己的ua池,去添加默认的请求头,

Ua有很多,这里就不放出来了,如果有兴趣可以直接去源码里面拿。直接说原理:构造很多个Ua,然后随机取用。从而降低这个同一访问频率:同时也暴露端口方便你自己传入header

1
2
3
4
5
6
7
8
9
from powerspider.tools.Ua import ua
import requests

def downloader(url, method=None, header=None, binary=False):
_headers = header if header else {'User-Agent': ua()}
_method = "GET" if not method else method
response = requests.request(url, method=_method, headers=_headers)
encoding = cchardet.detect(response.content)['encoding']
return response.content if binary else response.content.decode(encoding)

那么基本的文件都已经解决了,不过还不完美。异常处理,错误重试,日志什么都没。这怎么行呢。活既然干了,那就干的漂漂亮亮的。

来让我们加入进来这些东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import cchardet
from retrying import retry
from powerspider import logger
from powerspider.tools.Ua import ua
from requests import request, RequestException


@retry(stop_max_attempt_number=3, retry_on_result=lambda x: x is None, wait_fixed=2000)
def downloader(url, method=None, header=None, timeout=None, binary=False, **kwargs):
logger.info(f'Scraping {url}')
_header = {'User-Agent': ua()}
_maxTimeout = timeout if timeout else 5
_headers = header if header else _header
_method = "GET" if not method else method
try:
response = request(method=_method, url=url, headers=_headers, **kwargs)
encoding = cchardet.detect(response.content)['encoding']
if response.status_code == 200:
return response.content if binary else response.content.decode(encoding)
elif 200 < response.status_code < 400:
logger.info(f"Redirect_URL: {response.url}")
logger.error('Get invalid status code %s while scraping %s', response.status_code, url)
except RequestException as e:
logger.error(f'Error occurred while scraping {url}, Msg: {e}', exc_info=True)


if __name__ == '__main__':
print(downloader("https://www.baidu.com/", "GET"))

至此,我们的对Requests二次封装,构造通用的请求函数就已经完成了。

源码地址:https://github.com/PowerSpider/PowerSpider/tree/dev

期待下次再见

技术杂谈

前阵子有个朋友问我一个 Python 相关的问题,然后我把解决方案简单用代码写了一下发给他,结果他说跑不起来,我就很纳闷,确认了下他说 Python 库也都装了,但是运行的时候还是会报库找不到。果不其然,经过一段时间沟通发现,它的机器上装了两个 Python 环境,装 Python 库的时候是用的 pip 安装的,它对应了一个 Python 环境,但是运行的时候却是用的使用的 python3 命令却是对应另外一个 Python 环境,理所当然就跑不通了。这个问题其实挺常见的,有时候跟对方说检查下 Python 环境路径什么的,但是对方有时候并不知道怎么做,或者直接得再告诉他用 python3 -m pip install ... 命令来装包。

另外我还有个需求是,我写了一些代码,想直接把运行结果和代码发给别人看,一种方法可以直接借助于 GitHub,代码放上去,然后让对方 Clone 下来再运行。对方也得配置下相关的环境才能正确跑起来。另一种方法则是打包 Docker 镜像,但是这也得额外花一些时间。有没有更好的方案解决这个问题呢?

上面这就是两个问题:

  • 环境不一致的问题
  • 环境共享的问题

这俩问题有通用的解决方案吗?有!答案就是云 IDE。有了云 IDE,不仅以上两个问题可以解决,甚至都不用自己配置开发环境了,我们只需要一个浏览器就能在线编辑代码,同时这个 IDE 还能分享给他人,他人打开之后,看到的就是和我一模一样的内容,甚至还可以看到我正在编写代码的实时效果。

美哉美哉!

今天就给大家介绍一款云 IDE,是华为家的,叫做「华为云CloudIDE」,我试用了一下感觉还不错,它的官方介绍页面是这样的:

image-20201111005857785

官方介绍如下:

通过华为云CloudIDE,您仅需一个浏览器就可以编辑、调试、运行您的代码。

开发环境通常包括各种配置文件、特定的语言、框架版本、以及其他个人设备等等一系列个人配置。当您开发一个新的项目或者测试一些程序时,不得不对已有的个人配置做相应的调整。或者,您可以通过CloudIDE将个人的偏好设置转移到云端。CloudIDE将存储项目设置,实现您与他人共享,并通过浏览器授予访问的权限。您可以将所有文件保存在云环境中,并通过各种访问设备操作管理它们。开发环境在云端,您可以实现代码阅读、编写、调试、运行,也可以将代码存入代码仓库,随时、随地、随心!

和我刚才说的就是一样的,利用它我们可以解决诸多问题:如环境配置、环境统一、环境共享的问题,同时我们还可以随时随地编辑代码,丝毫不受机器的限制,上云之后,一切都是那么方便。

好,这里我就带大家来体验下「华为云CloudIDE」吧。

首先,目前「华为云CloudIDE」是可以免费体验的,每个人每天有 120 分钟的免费体验时间,如果要获得更好的体验可以创建付费实例。

目前免费体验需要有这些规定:

尊敬的用户,您已经开通CloudIDE服务,免费体验需要遵守如下约定:

  • 每天的免费体验时间为120分钟,少于5分钟不能创建新的体验;
  • 每个实例可使用时长为60分钟,60分钟后,实例将会被删除,并且数据将会被删除;
  • 同一时间只能体验一个实例,打开第二个实例将会自动关闭第一个实例;
  • 免费体验不能保证最佳使用体验,您可能需要排队等候,付费使用可以获得更好的体验

完全没问题,体验完全足够了。

我这里操作的 URL 是 https://devcloud.cn-north-4.huaweicloud.com/cloudide/trial,大家可以注册个华为云账号就能看到对应的入口页面了,就是上文发的图。

由于我对 Python 比较熟悉,那么我就来体验一个 Python 环境吧,找到下方的 Python 环境,然后点击「免费体验」。

这时候环境就会变成「启动中」的状态,如图所示:

image-20201111010644754

然后接下来我们就会进入到云 IDE 的环境,等待一顿初始化之后之后,我们就可以看到如下页面,如图所示:

image-20201111010740454

嗯?有没有熟悉的感觉?

没错,这和 VS Code 可太像了,简直就是一个在线版的 VS Code。不过仔细看看,其实还是有些不一样的,比如左上角的 Logo,插件仓库以及一些菜单选项。可以说是 VS Code 的魔改版。

我们看下左侧栏其他的菜单,搜索、版本控制、调试没啥好说的,看看插件仓库吧,打开之后是这样的:

image-20201111011151570

这里我们可以看到「华为云CloudIDE」已经为我们预装了一些插件,如 Python。另外下方还有一些推荐的插件,例如 Huawei Cloud Toolkit,这些插件的安装方式和 VS Code 是一模一样的。

接下来我们再看看菜单的一些配置,随便挑几个看看吧:

image-20201111011434518

image-20201111011453092

几乎所有功能菜单和 VS Code 是一样的,不过「华为云CloudIDE」添加了一些入口,比如 Close Instance 等等,点了之后就会关闭当前云 IDE 实例。

OK,接下来我们再回到代码看看,看看它给初始化了个什么,代码结构是这样的:

image-20201111011717465

就是一个基本的 Flask 的 Hello World。

那来跑跑试试吧,点击右上角的运行按钮,如图所示:

image-20201111011824402

哦,居然报错了,内容如下:

image-20201111011856800

这里报错说找不到 flask 这个包,那很明显就是没安装了,正好我们可以看到 requirements.txt 定义了相关的依赖环境,那我们来手动安装下吧。

在当前命令行下输入:

1
pip3 install -r requirements.txt

运行结果类似如下:

image-20201111012030928

看起来不错,另外可以观察到它是从 https://repo.huaweicloud.com/repository/pypi/ 这里下载的 Python 包,速度也挺快的。简直是意外收获呢,又发现了一个不错的 Pypi 加速镜像。

顺便看看 pip.conf 配置吧:

1
cat ~/.pip/pip.conf

结果如下:

1
2
3
4
5
[global]
index-url = https://repo.huaweicloud.com/repository/pypi/simple
trusted-host = repo.huaweicloud.com
timeout = 120
extra-index-url = https://obs-workspace-20180810.obs.cn-north-1.myhuaweicloud.com/pypi/simple

一目了然,可以看到 pip 全局配置了一个加速镜像,index-url 配置为了 https://repo.huaweicloud.com/repository/pypi/simple,这个是可以公开访问的,所以以后

我们装包的时候就可以这么来装了:

1
pip3 install requests -i https://repo.huaweicloud.com/repository/pypi/simple

速度还是挺不错的,美滋滋,意外收获。

OK,切回正题,这时候我们已经装完环境了,重新运行 app.py,这时候就可以发现运行成功了,结果类似如下:

image-20201111012729299

可以看到它就在 8080 端口上运行了一个服务,那咋访问呢?

在运行的同时,还弹出了这么一个窗口,如图所示:

image-20201111012846946

似乎是提示我们可以注册一个外部端口将服务暴露出去,我们点击 Register 和 OK。

接下来 OK 按钮就变成了 Access 按钮,如图所示:

image-20201111013057453

还带了一个二维码和 Copy 按钮,我们 Copy 一下看看,我 Copy 之后的 URL 是 https://sermgcaktl-8080-cce-5.lf.templink.dev/,然后我在浏览器中打开,显示如下:

image-20201111013157227

很不错啊,这是自动给我们生成了一个域名,然后还把服务暴露出去可供访问了,挺好。

接下来我们再看看 Workspace 吧,切换下代码文件夹,点击 File - Open ,可以看到如下页面:

image-20201111013747428

可以看到当前打开的是 python3-sample 这个文件夹,那能换其他文件夹吗?我在上方输入个根目录 / 看看,结果如图所示:

image-20201111013852795

出现了熟悉的 Linux 文件树结构,那可以断定这其实就是一个 Linux 环境了,那岂不是干啥事都行了。

找到 /home/user/projects 目录,看起来是用来专门存放项目的目录。

另外我再试试能否随便打开一个 GitHub 上的项目吧,比如在控制台进入刚才这个目录,然后 Clone 一个项目,命令如下:

1
2
cd /home/user/projects/
git clone https://github.com/Python3WebSpider/ScrapyTutorial.git

OK,简单 Clone 了一个 Scrapy 项目,成功!如图所示:

image-20201111014223373

然后打开下这个项目,如图所示:

image-20201111015026872

打开 Terminal,安装下依赖:

1
pip3 install scrapy pymongo

另外这个项目依赖于 MongoDB,不过我们这里仅仅测试,可以取消它的依赖,setting.py 中注释掉如下代码:

1
2
3
4
5
ITEM_PIPELINES = {
'tutorial.pipelines.TextPipeline': 300,
- 'tutorial.pipelines.MongoDBPipeline': 400,
+ #'tutorial.pipelines.MongoDBPipeline': 400,
}

然后接下来可以运行如下命令启动爬虫了:

1
scrapy crawl quotes

这时候就可以看到爬虫就成功运行了,如图所示:

image-20201111015327574

完美,Clone 的项目也成功运行了,美滋滋。

最后我们看看这个退出之后怎么样,先复制下当前的 URL,我的 URL 是 https://ide.devcloud.cn-north-4.huaweicloud.com/cce-5/sermgcaktl/#/home/user/projects/ScrapyTutorial,然后我们直接关掉当前页面选项卡,然后重新进入,看看现在的运行状态还在不在。

牛逼!重新进入之后还是原来的样子,控制台依然还是开的,跑的结果也依然有!

image-20201111015741812

很好!

那这个环境怎么分享给别人呢?看了下「华为云CloudIDE」是支持分享功能的,这里可以进入到 CloudIDE 的首页,点击管理,然后通过白名单或者黑名单的方式添加用户就可以了。

image-20201111020023870

好了,到现在为止「华为云CloudIDE」就体验完了,感觉整体上还是挺不错的,大家也可以来体验试试看吧!

技术杂谈

YAML文件概述

K8s集群文件中对资源管理和资源对象编排部署都可以通过声明样式yaml,文件来解决,也就是说可以把需要对资源对象操作编辑到yaml,文件中。

我们称之为资源清单资源清单文件通过kubectl命令直接使用资源清单文件就可以实现对大量资源对象进行编排部署

基本语法

  • 大小写敏感
  • 使用缩进表示层级关系,缩进不允许使用tab,只允许空格
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • ‘#’表示注释
  • ---表示新的yaml文件的开始

    数据类型

    YAML 支持以下几种数据类型:
  • 对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)
  • 数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)
  • 纯量(scalars):单个的、不可再分的值

    常量

    常量是最基本的,不可再分的值,包括:
  • 字符串
  • 布尔值
  • 整数
  • 浮点数
  • Null
  • 时间
  • 日期

    引用

    & 锚点和 * 别名,可以用来引用:

    & 用来建立锚点(defaults),<< 表示合并到当前数据,* 用来引用锚点。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    defaults: &defaults
    adapter: postgres
    host: localhost
    development:
    database: myapp_development
    <<: *defaults
    test:
    database: myapp_test
    <<: *defaults
    ---相当于
    defaults:
    adapter: postgres
    host: localhost
    development:
    database: myapp_development
    adapter: postgres
    host: localhost
    test:
    database: myapp_test
    adapter: postgres
    host: localhost

    kubernetes中yaml组成部分

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    apiVersion: v1								# API版本 可使用命令kubeclt api--verison查看
    kind: ReplicationController # 资源类型 - 副本控制器RC
    metadata: # 资源元数据
    name: mysql # - RC的名称(全局唯一),符合目标的Pod拥有此标签
    spec: # 资源的规格(RC的相关属性的定义)
    replicas: 1 # 副本的期望数量
    selector: # 标签选择器
    app: mysql
    # *********************************************
    template: # Pod 模版
    metadata:
    name: mysql
    labels: # 标签 Pod 副本拥有的标签,对应RC的Selector
    app: mysql
    spec: # Pod规格
    containers: # 容器的配置
    - name: mysql # 容器名称
    image: mysql # 容器镜像(对应的Docker images)
    ports:
    - containerPort: 3306 # 容器引用监听的端口号
    env: # 环境配置
    - name: MYSQL_ROOT_PASSWORD
    value: "123456"


    ---
    apiVersion: v1
    kind: Service # 资源类型 服务
    metadata:
    name: mysql
    spec:
    selector:
    app: mysql
    ports:
    - port: 3306
  • 控制器部分
    控制器部分
  • 被控制的对象
    被控制的对象
  • RC
    RC

    快速编写yaml文件

    Part 1:使用命令生成yaml文件

    1
    2
    3
    4
    5
    6
    # kubectl create 
    kubectl create deployment web --image=nginx -o yaml --dry-run
    # -o 使用yaml格式展示
    # -dry-run 尝试运行,并不会真正运行
    # 保存至myweb.yaml
    kubectl create deployment web --image=nginx -o yaml --dry-run > myweb.yaml
  • kubectl create deployment web —image=nginx -o yaml —dry-run运行效果如下⬇️:
    kubectl create

    Part 2: 使用命令导出yaml文件

    1
    2
    3
    kubectl get
    kubectl get deploy # 查看部署
    kubectl get deploy nginx -o yaml --export > myweb.yaml

kubectl命令学习

这里简单记录下 Kubectl 部署的一些准备工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
Basic Commands (Beginner):
create # Create a resource from a file or from stdin.
expose # 使用 replication controller, service, deployment 或者 pod 并暴露它作为一个 新的
Kubernetes Service
run # 在集群中运行一个指定的镜像
set # 为 objects 设置一个指定的特征

Basic Commands (Intermediate):
explain # 查看资源的文档
get # 显示一个或更多 resources
edit # 在服务器上编辑一个资源
delete # Delete resources by filenames, stdin, resources and names, or by resources and label selector

Deploy Commands:
rollout # Manage the rollout of a resource
scale # 为 Deployment, ReplicaSet, Replication Controller 或者 Job 设置一个新的副本数量
autoscale # 自动调整一个 Deployment, ReplicaSet, 或者 ReplicationController 的副本数量

Cluster Management Commands:
certificate # 修改 certificate 资源.
cluster-info # 显示集群信息
top # Display Resource (CPU/Memory/Storage) usage.
cordon # 标记 node 为 unschedulable
uncordon # 标记 node 为 schedulable
drain # Drain node in preparation for maintenance
taint # 更新一个或者多个 node 上的 taints

Troubleshooting and Debugging Commands:
describe # 显示一个指定 resource 或者 group 的 resources 详情
logs # 输出容器在 pod 中的日志
attach # Attach 到一个运行中的 container
exec # 在一个 container 中执行一个命令
port-forward # Forward one or more local ports to a pod
proxy # 运行一个 proxy 到 Kubernetes API server
cp # 复制 files 和 directories 到 containers 和从容器中复制 files 和 directories.
auth # Inspect authorization

Advanced Commands:
diff # Diff live version against would-be applied version
apply # 通过文件名或标准输入流(stdin)对资源进行配置
patch # 使用 strategic merge patch 更新一个资源的 field(s)
replace # 通过 filename 或者 stdin替换一个资源
wait # Experimental: Wait for a specific condition on one or many resources.
convert # 在不同的 API versions 转换配置文件
kustomize # Build a kustomization target from a directory or a remote url.

Settings Commands:
label # 更新在这个资源上的 labels
annotate # 更新一个资源的注解
completion # Output shell completion code for the specified shell (bash or zsh)

Other Commands:
api-resources # Print the supported API resources on the server
api-versions # Print the supported API versions on the server, in the form of "group/version"
config # 修改 kubeconfig 文件
plugin # Provides utilities for interacting with plugins.
version # 输出 client 和 server 的版本信息

Kubectl create

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Create a resource from a file or from stdin.

JSON and YAML formats are accepted.

Examples:
# Create a pod using the data in pod.json.
kubectl create -f ./pod.json

# Create a pod based on the JSON passed into stdin.
cat pod.json | kubectl create -f -

# Edit the data in docker-registry.yaml in JSON then create the resource using the edited data.
kubectl create -f docker-registry.yaml --edit -o json

Available Commands:
clusterrole # Create a ClusterRole.
clusterrolebinding # 为一个指定的 ClusterRole 创建一个 ClusterRoleBinding
configmap # 从本地 file, directory 或者 literal value 创建一个 configmap
cronjob # Create a cronjob with the specified name.
deployment # 创建一个指定名称的 deployment.
job # Create a job with the specified name.
namespace # 创建一个指定名称的 namespace
poddisruptionbudget # 创建一个指定名称的 pod disruption budget.
priorityclass # Create a priorityclass with the specified name.
quota # 创建一个指定名称的 quota.
role # Create a role with single rule.
rolebinding # 为一个指定的 Role 或者 ClusterRole创建一个 RoleBinding
secret # 使用指定的 subcommand 创建一个 secret
service # 使用指定的 subcommand 创建一个 service.
serviceaccount # 创建一个指定名称的 service account

Options:
--allow-missing-template-keys=true: If true, ignore any errors in templates when a field or map key is missing in the template. Only applies to golang and jsonpath output formats.
--dry-run=false: If true, only print the object that would be sent, without sending it.
--edit=false: Edit the API resource before creating
-f, --filename=[]: Filename, directory, or URL to files to use to create the resource
-k, --kustomize='': Process the kustomization directory. This flag can't be used together with -f or -R.
-o, --output='': Output format. One of:
json|yaml|name|go-template|go-template-file|template|templatefile|jsonpath|jsonpath-file.
--raw='': Raw URI to POST to the server. Uses the transport specified by the kubeconfig file.
--record=false: Record current kubectl command in the resource annotation. If set to false, do not record the
command. If set to true, record the command. If not set, default to updating the existing annotation value only if one
already exists.
-R, --recursive=false: Process the directory used in -f, --filename recursively. Useful when you want to manage
related manifests organized within the same directory.
--save-config=false: If true, the configuration of current object will be saved in its annotation. Otherwise, the
annotation will be unchanged. This flag is useful when you want to perform kubectl apply on this object in the future.
-l, --selector='': Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)
--template='': Template string or path to template file to use when -o=go-template, -o=go-template-file. The
template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview].
--validate=true: If true, use a schema to validate the input before sending it
--windows-line-endings=false: Only relevant if --edit=true. Defaults to the line ending native to your platform.

Usage:
kubectl create -f FILENAME [options]

技术杂谈

最近博客换上了 Hexo,但是发现经常莫名其妙构建失败,报内存溢出,Out of Memory,错误如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
==== JS stack trace =========================================

0: ExitFrame [pc: 0x1409219]
Security context: 0x36ffd21408d1 <JSObject>
1: get sticky [0x36ffd2149801](this=0x1e69c6f730e1 <JSRegExp <String[10]: [\t \n\r]+>>)
2: match [0x6ca6396ae39] [/github/workspace/node_modules/js-beautify/js/src/core/inputscanner.js:~110] [pc=0x1d19a16be4dc](this=0x1e69c6f73119 <InputScanner map = 0x1c206a47a549>,0x1e69c6f730e1 <JSRegExp <String[10]: [\t \n\r]+>>)
3: tokenize [0x6ca6396e171] [/gith...

1: 0xa17c40 node::Abort() [hexo]
2: 0xa1804c node::OnFatalError(char const*, char const*) [hexo]
3: 0xb95a7e v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [hexo]
4: 0xb95df9 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [hexo]
5: 0xd53075 [hexo]
6: 0xd53706 v8::internal::Heap::RecomputeLimits(v8::internal::GarbageCollector) [hexo]
7: 0xd5ffc5 v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [hexo]
8: 0xd60e75 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [hexo]
9: 0xd6251f v8::internal::Heap::HandleGCRequest() [hexo]
10: 0xd10f85 v8::internal::StackGuard::HandleInterrupts() [hexo]
11: 0x106c5c6 v8::internal::Runtime_StackGuard(int, unsigned long*, v8::internal::Isolate*) [hexo]
12: 0x1409219 [hexo]

原因就是内存溢出了,这是因为 Hexo 在构建的时候存了一个特别大的数组,而默认 Node 运行时最大内存为 512 MB,详情可以见:https://github.com/hexojs/hexo/issues/2165。

真是服了,才几百篇文章就不行了,耗费内存至于这么多吗?

解决方案,可以直接设置 Node 的最大内存限制,比如我直接设置为 16G,在构建之前执行如下命令就行了:

1
export NODE_OPTIONS="--max-old-space-size=16384"

这样就设置了 Node 运行时的最大内存,就不会触发内存溢出了。

Markdown

在写文章的时候我经常会遇到这么一个需求,我想要跟大家说明某一行代码需要改动成另外一行代码。

比如我这里有一段代码:

1
2
3
4
5
6
FROM python:3.6
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
VOLUME ["/app/proxypool/crawlers/private"]
CMD ["supervisord", "-c", "supervisord.conf"]

仅仅作为示例,这里行号我也没标,行数也有可能很多行,比如上百行。

比如我就想告诉大家,我想把:

1
RUN pip install -r requirements.txt

改成:

1
RUN pip install -r requirements.txt -i https://pypi.douban.com/simple

这一行。

看,我已经说完了。

为了说这件事,我需要打好几行字,我得把原来的代码打上,然后说「改成」,然后再把新代码打上。

麻烦吧?

有没有什么更好的表述方式呢?

有。

看这里:

1
2
-RUN pip install -r requirements.txt
+RUN pip install -r requirements.txt -i https://pypi.douban.com/simple

是不是很直观?

红色代表删减,绿色代表增加。

经常 Code Review 的朋友一定倍感亲切。

那这个怎么实现的呢?其实这就是用了 Markdown 高亮的一种写法,只需要把语言改成 diff 就好了。

原语言如下:

这里为了防止 Markdown 解析冲突,我就用图片了。

反正就是语言标注改成 diff,然后需要删除的前面加个减号,需要增加的加个加号就行了。

完毕!

希望有帮助!

技术杂谈

kubernetes-install

操作系统初始化

  • 关闭防火墙(all)
1
2
3
4
5
6
# 临时关闭防火墙
systemctl stop firewalld
# 永久关闭防火墙
systemctl disable firewalld
# 验证
systemctl status firewalld
  • 关闭selinux(all)
1
2
3
4
# 临时关闭
setenforce 0
# 永久
sed -i 's/enforcing/disabled/' /etc/selinux/config
  • 关闭swap(all)
1
2
3
4
# 临时
swapoff -a
# 永久
sed -ri 's/.*swap.*/#&/' /etc/fstab
  • 设置主机名称(all)
1
2
3
4
# 设置名称(k8s-m-1)忽略大写字母
hostnamectl set-hostname k8s-m-1
# 验证
hostname
  • Master添加Hostname(master)
1
2
3
4
5
6
7
8
9
10
11
12
# 设置
cat >> /etc/hosts << EOF
masterIp master
node1Ip node1
node2Ip node2
EOF
# eg
cat >> /etc/hosts << EOF
192.168.50.212 k8s-m-1
192.168.50.87 k8s-n-1
192.168.50.85 k8s-n-2
EOF
  • 将桥接的IPV4 流量传递到iptables的链(all)
1
2
3
4
5
6
cat > /etc/sysctl.d/k8s.conf << EOF
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF
# 生效
sysctl --system
  • 时间同步(All)
1
2
3
yum install -y ntpdate 
ntpdate time.windows.com
# 三台机子输出如下则成功(相差几秒或几分为正常现象)

安装Docker

官方文档-安装

  • Docker安装sh Script:(All)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# You can use scripts for one click installation,You may need to type enter at the end
# remove docker
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
# Set up repository
sudo yum install -y yum-utils
# Use Aliyun Docker
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# install docker from yum
yum install -y docker-ce docker-ce-cli containerd.io
# restart docker
systemctl restart docker
# cat version
docker --version

  • 配置加速(all)
j
1
2
3
4
5
6
7
8
9
10
11
12
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://etdea28s.mirror.aliyuncs.com"]
}
EOF

# reload
sudo systemctl daemon-reload
sudo systemctl restart docker

# 检查阿里云加速

kubernetes安装

  • 配置kubernetes源(all)
1
2
3
4
5
6
7
8
9
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF

由于官网kubernetes源在国外有墙,直接使用官方源会导致安装失败。所以我们配置国内的阿里源

  • 安装 kubectl kubelet kubeadm(all)
1
2
3
4
# install kubectl kubelet kubeadm
yum install -y kubectl kubelet kubeadm
# set boot on opening computer
systemctl enable kubelet
  • 初始化k8s部署(Master)
1
2
3
4
5
6
7
8
9
10
11
12
13
kubeadm init \
--apiserver-advertise-address=youselfIp of Master \
--image-repository registry.aliyuncs.com/google_containers \
# 不冲突即可
--service-cidr=10.10.0.0/16 \
--pod-network-cidr=10.122.0.0/16

# eg
kubeadm init \
--apiserver-advertise-address=192.168.50.212 \
--image-repository registry.aliyuncs.com/google_containers \
--service-cidr=10.10.0.0/16 \
--pod-network-cidr=10.122.0.0/16

常见错误:running with swap on is not supported. Please disable swap

[preflight] If you know what you are doing, you can make a check non-fatal with `—ignore-preflight-

errors=…`

原因:系统自动进行分区

解决:

1
2
3
4
# 临时
swapoff -a
# 永久
sed -ri 's/.*swap.*/#&/' /etc/fstab
  • following as a regular user(Master)
1
2
3
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
  • join master node(node)
1
kubeadm join 172.16.164.136:6443 --token 9oilao.bpbxcm5zkk0jjcgm --discovery-token-ca-cert-hash sha256:609794bd03915be382bdb130c4c180e89cdc863d35cf99be79cf4ddcbfacee24

加入成功,如下图

此时我们在Master节点上使用命令kubectl get nodes查看节点信息:如下图所示

此时的kubectl get nodes的status都是NotNotReady:

查看kubernetes运行状态:

kubectl get pods -n kube-system

如图:

果然,两个Pending犹豫未决

此时我们部署CNI网络,配置如下

1
2
3
# 根据官方文档提示配置CNI网络
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
# 报错:The connection to the server raw.githubusercontent.com was refused - did you specify the right host or port? 原因:外网不可访问 -> 在https://www.ipaddress.com/查询raw.githubusercontent.com的真实IP。
1
2
3
sudo vi /etc/hosts
199.232.28.133 raw.githubusercontent.com
# 如下

1
2
3
4
5
# 开启IPVS,修改ConfigMap的kube-system/kube-proxy中的模式为ipvs

kubectl edit cm kube-proxy -n kube-system
# 将空的data -> ipvs -> mode中替换如下
mode: "ipvs"

在此运行kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml成功,如图

此时运行kubectl get nodes效果图如下->成功。(肯能并不一定会立马成功,上面👆确定没问题,请稍等片刻即可)

测试kubernetes

1
2
3
4
5
6
7
# 创建nginx镜像 Create a deployment with the specified name
# kubectl create deployment NAME --image=image -- [COMMAND] [args...] [options]
kubectl create deployment nginx --image=nginx
# 对外暴露端口
kubectl expose deployment nginx --port=80 --type=NodePort
# 查看pod服务
kubectl get pod,svc

成功

技术杂谈

最近刚遇到个问题,我要给自己做的网站加个无限 debugger 反爬,网站是基于 Vue.js 开发的,比如我就想在 main.js 里面加这么一段:

1
2
3
4
setInterval(() => {
debugger
console.log('debugger')
}, 1000)

当时在 Debug 环境下一切好好的,但是到了 build 之后,再运行发现 debugger 就没了,这就神奇了。

我搜了很久,最后终于找到了解决方案,这里记录下。

开发环境和生产环境

这里首先说下 Vue.js 是有开发环境和生产环境之分的,这里它用了一个关键词,叫做 mode。

我们先来看看两个常用的命令:

1
2
npm run serve
npm run build

这两个命令如果大家开发 Vue.js 的话一定不会陌生,但它们是怎么实现的呢?

顺着一找,其实他们定义在了 package.json 里面,是这样的:

1
2
3
4
5
6
7
8
9
10
{
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
...
}

这里其实就是调用了 vue-cli-service 的几个命令而已。

vue-cli-service 又是哪里来的呢?很简单,在刚开始初始化项目的时候装了个 Vue CLI:

1
2
3
npm install -g @vue/cli
# OR
yarn global add @vue/cli

它提供了 vue-cli-service 这个命令。

然后我们再来详细看看这个 serve 和 build 命令。

serve

用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
Usage: vue-cli-service serve [options] [entry]

Options:

--open open browser on server start
--copy copy url to clipboard on server start
--mode specify env mode (default: development)
--host specify host (default: 0.0.0.0)
--port specify port (default: 8080)
--https use https (default: false)
--public specify the public network URL for the HMR client
--skip-plugins comma-separated list of plugin names to skip for this run

看到了吧,这里有个 mode,指的就是运行环境,这里默认为 development,即开发环境。

build

用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Usage: vue-cli-service build [options] [entry|pattern]

Options:

--mode specify env mode (default: production)
--dest specify output directory (default: dist)
--modern build app targeting modern browsers with auto fallback
--no-unsafe-inline build app without introducing inline scripts
--target app | lib | wc | wc-async (default: app)
--formats list of output formats for library builds (default: commonjs,umd,umd-min)
--inline-vue include the Vue module in the final bundle of library or web component target
--name name for lib or web-component mode (default: "name" in package.json or entry filename)
--filename file name for output, only usable for 'lib' target (default: value of --name),
--no-clean do not remove the dist directory before building the project
--report generate report.html to help analyze bundle content
--report-json generate report.json to help analyze bundle content
--skip-plugins comma-separated list of plugin names to skip for this run
--watch watch for changes

这里也有一个 mode,默认就是 production 了,即生产环境。

所以,到这里我们就明白了,调用 build 命令之后,实际上是生产环境了,然后生产环境可能做了一些特殊的配置,把一些 debugger 给去除了,所以就没了。

还原

那咋还原呢?

这里我们就需要用到 Vue.js 的另外一个知识了。

Vue.js 同样是基于 Webpack 构建的,利用了 Webpack 的打包技术,不过为了更加方便开发者配置,Vue.js 在 Webpack 的基础上又封装了一层,一些配置我们不需要再实现 webpack.config.js 了,而是可以实现 vue.config.js,配置更加简单。

在 vue.config.js 里面,它为 Webpack 暴露了几个重要的配置入口,一个就是 configureWebpack,一个是 chainWebpack。

具体的教程大家可以参考官方文档:https://cli.vuejs.org/zh/guide/webpack.html。

比如前者可以这么配置:

1
2
3
4
5
6
7
module.exports = {
configureWebpack: {
plugins: [
new MyAwesomeWebpackPlugin()
]
}
}

后者可以这么配置:

1
2
3
4
5
6
7
8
9
10
11
12
// vue.config.js
module.exports = {
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.loader('vue-loader')
.tap(options => {
return options
})
}
}

这里,我们如果要修改 Webpack 构建的一些配置的话,可以利用 chainWebpack。

TerserPlugin

然后,这里又需要引入另外一个 Webpack 插件了,叫做 TerserPlugin,官方介绍链接为 https://webpack.js.org/plugins/terser-webpack-plugin/。

而这个库又是依赖 terser 的,官方介绍链接为 https://github.com/terser/terser。

官方介绍为:

A JavaScript parser and mangler/compressor toolkit for ES6+.

OK,反正就是类似一个 JavaScript 压缩转换器,比如它可以将一些 JavaScript 代码转码、混淆、压缩等等。

这里我们就需要借助于它来实现 debugger 的还原。

这里由于我使用的 Webpack 是 4.x 版本,所以 TerserPlugin 也需要是 4.x 版本,5.x 版本我测试过了不行。

安装配置如下,添加到 package.json 的 devDependencies 里面:

1
"terser-webpack-plugin": "^4.2.3",

然后:

1
npm install

接着 vue.config.js 改写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
...
productionSourceMap: false,
chainWebpack: config => {
config.optimization.minimizer([
new TerserPlugin({
terserOptions: {
mangle: true,
compress: {
drop_debugger: false
}
}
})
]
)
}
}

这里我就保留了 chainWebpack 的配置,然后这里面通过 config 的 optimization 的 minimizer 方法配置了 plugins,然后这里 TerserPlugin 需要声明一个 terserOptions,然后 compress 里面的 drop_debugger 需要设置为 false。

这样,生产环境的 debugger 语句就不会丢了。

技术杂谈

2019 年,网红王尼玛做了一个五分钟的视频,是有关程序员的综艺节目《创造1024》,视频冲上热搜,被转发了几十万次,而这个视频,通篇是对程序员的刻板印象和标签化再加深。

视频里,程序员是这样子的:

视频里面每个程序员都是标配的黑眼圈,更有甚者那个连续七天七夜不眠不休最后累倒的程序员被推崇为 C 位大佬。「没女朋友」、「宅男」、「呆板」、「理工男」、「格子衫」、「没头发」 等等这些标签。

我不是吐槽视频本身,通过这个视频,站在另外的角度去思考,这里面确实有很多让我感觉不那么舒服的地方。

  • 第一,把女性程序员排除在外;
  • 第二,把整个行业的形象刻画的呆板、邋遢;
  • 第三,为什么程序员就一定会被标签化为这样的形象;

有的时候真的非常伤心,互联网时代真的来临了,程序员这个行业走上台前了。

然而走上台前的,不是曾经想用产品改变世界的梦想,而是秃头、加班、格子衫。

看到一些微博还有这样的:

所以一直到现在,我都对格子衫有着非常深刻的生理性反感,我有条纹衬衫,也有花衬衫,但就是不喜欢格子衫,因为每当我去穿格子衫的时候,我就觉得我走进了公众预设定的无聊框架中。因此,每当女朋友陪我买衣服的时候,路过格子衫的区域,我一定不会去选。

嘿嘿没错,我还有女朋友,谁说程序员没有女朋友的?况且,我女朋友还那么好看。

哦哦跑题了,说回格子衫,我现在选择不穿格子衫,其实也是被框架捆绑的一部分,可我又能怎么办呢?

当然,logo卫衣还是很赞。

当然,在提到程序员这个行业的时候,最大也是最让我讨厌的刻板影响,叫程序员安慰师。

程序员安慰师这个所谓的行业和所谓的热搜热转出来的时候,是我觉得最荒谬的事情,到底是谁觉得我们的智商和情商,我们对工作的热情和严谨,是被女性带动的。每个男性都喜欢漂亮的女孩,这是原始的性,但在这之上,我们如此优秀完成工作的事情,是我们聪明的大脑(掐腰)。

最后我想说,我仍然能看到有非常多的调研报告,有越来越多的小姐姐开始在相亲中去倾向程序员这个行业。

但我想说,所谓高薪,所谓单纯,所谓不会出轨,都是外界给我们的一个个贴下的标签,而在这标签下,我们仍然是独立而特别的个体。希望你最后的选择是因为我这个人,而并不是那些无聊的刻板印象。

我们程序员并不一定是秃头、呆滞、毫无生活情趣的死肥宅,至少说很多都不是,我们也有可爱的一面,也有自己热爱的生活,喜欢的人,喜欢的事,请行业不要再这么标签化我们。

1024,我既不想接受格子衫与人体工学椅的安利,也不想接受任何《程序员图鉴》的调侃。我希望节日能让公司反思过高强度的加班,我希望节日能给更多的人传输目前程序届的开源思想,我希望节日能让人们回忆起,在疫情时候,有那么多个体的程序员不记薪资帮助做统计和维护框架,我希望节日能让更多人意识到我们每一个有灵魂的个体本身。

最后,整篇发言稿都是我的一家之言,是我的三观输出,可能会有很多朋友不习惯或者不开心。用程序码出世界,始终是我的理想。

无论如何,祝大家 1024 节日快乐!

使用Hexo编写博客?

亲爱的伙伴您好,很荣幸能与您在此相遇.

本文主要用于记录,在一台完全的“新”机子上,书写Hexo部署博客。

我已经迫不及待想告诉你的几个容易错误的点,你准备好了么?

Let’s Go

在此之前说明,很多东西都是来自网络,当然无论如何还是建议你查阅官方文档。

Hexo官方文档

Fisrt 环境准备

  • [ ] Git
  • [ ] Node.js (Node.js 版本需不低于 10.13,建议使用 Node.js 12.0 及以上版本)
  • [ ] Hexo

Git:

可以参考此文章 https://cuiqingcai.com/9336.html

Mac 用户

如果在编译时可能会遇到问题,请先到 App Store 安装 Xcode,Xcode 完成后,启动并进入 Preferences -> Download -> Command Line Tools -> Install 安装命令行工具。

Node.JS:

Node.js 为大多数平台提供了官方的 安装程序。对于中国大陆地区用户,可以前往 淘宝 Node.js 镜像 下载。

其它的安装方法:

  • Windows:通过 nvs(推荐)或者nvm 安装。
  • Mac:使用 HomebrewMacPorts 安装。
  • Linux(DEB/RPM-based):从 NodeSource 安装。
  • 其它:使用相应的软件包管理器进行安装,可以参考由 Node.js 提供的 指导

Hexo:

所有必备的应用程序安装完成后,即可使用 npm 安装 Hexo。

1
2
3
4
# 和我一样小白的可使用以下命令来安装
npm install -g hexo-cli
# 对于熟悉 npm 的大牛,可以仅局部安装 hexo 包。
npm install hexo

验证安装

1
2
# 注意请不要在项目中使用,可能会因为限制从而导致验证误差
hexo version

Look

Hexo Version

Second 写作

新建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 创建一篇新文章或者新的页面
hexo new [layout] <title>
# EG 注意双引号中的是博客标题,不需要加.md等后缀。
hexo new "Your Title"

# hexo new --help
Usage: hexo new [layout] <title>

Description:
Create a new post.

Arguments:
layout Post layout. Use post, page, draft or whatever you want.
title Post title. Wrap it with quotations to escape.

Options:
-p, --path Post path. Customize the path of the post.
-r, --replace Replace the current post if existed.
-s, --slug Post slug. Customize the URL of the post.

Hexo 有三种默认布局:postpagedraft。在创建者三种不同类型的文件时,它们将会被保存到不同的路径;而您自定义的其他布局和 post 相同,都将储存到 source/_posts 文件夹。

布局 路径
post source/_posts
page source
draft source/_drafts

文内设置

用markdown等编辑器写博客,tags的写法如下:

1
2
# 注意冒号与方括号之间有一个空格,方括号中的标签用英文的”,”
tags: [hexo,备忘录]

添加“阅读全文”按钮

方法一:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 在文章任意你想添加的位置添加
<!--more-->
# EG

---
title: How to use hexo to create blog?
date: 2020-10-17 01:48:53
author:Payne
Mail:wuzhipeng1289690157@gamil.com
tags:[Hexo]
---
<!--more-->
后面的内容在首页不显示,只显示到<!--more-->这里

方法二:

1
2
3
4
# 设置首页文章以摘要形式显示,打开主题配置文件,找到auto_excerpt进行修改
auto_excerpt:
enable: true
length: 150

其中length代表显示摘要的截取字符长度。
注:这两种方法,在博客首页显示的效果不一样,根据自己的需要,选择自己喜欢的方法

请开始您的表演😊

Show Time 。。。

Third 自我查阅

1
2
3
4
5
6
7
8
9
10
11
12
# 构建 -> 用于生成博客的html文件
hexo g
# 预览 -> 用于在本地预览博客,打开浏览器,输入 localhost:4000/ 即可查看。
hexo s
# 检查博客格式等符合要求后,用此命令将博客推送到远端。(需要是自己的才行)
hexo d

# 实操
# 构建并查阅
hexo g && hexo s
# 推送(记得清除缓存!!!)
hexo clean && hexo g && hexo d

images

Fourth 发布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 详细流程如下
首先clone下来然后,创建分支
git clone "目标地址"
git checkout -b branch_name


# 编写博客



# 构建并查阅
hexo g && hexo s
# 推送(记得清除缓存!!!)
hexo clean && hexo g && hexo d

技术杂谈

Abstract

随着大数据人工智能时代的来临,互联网的快速发展。许许多多以前可能并不那么实际或需要的算法、技术也逐渐进入我们的眼中。例如分布式、集群、负载均衡、也越来越“平民”化。近期重新再一次的对于分布式理念、思想进行了学习。此随笔也因此而来。请多指教 为什么需要分布式? 什么是分布式? 分布式的核心理念是什么? 如何实现分布式、负载均衡、集群?

Why distributed?

为什么需要分布式、集群、负载均衡?

概念的提出或理论的出现,一定是为了解决对应的问题或避免相对应的问题。使其不断的完善化、稳定化。毕竟Human nature is lazy 此时此刻可能更需要什么?(3V3H)

  • Volume 海量
  • Variety 多样
  • velocity 实时
  • 高并发
  • 高可扩
  • 高性能

单机可能将会遇到的问题会有

  • 系统容量
  • 单点故障
  • 性能不足

该如何解决此问题? 有一种比较功利性的思想-缺啥补啥,当然也是比较较为明确的方式。至少我仅仅只需要解决以上中较少问题。 分布式(思想)要解决的问题主要是单机系统中系统容量不足及提高系统可用性。

What‘s Distributed?

什么是分布式?亦或者说满足如何的条件才算分布式?

分布式(系统),简而言之即是由多个处理机通过通信路线互联而构成的松散。一次性近乎解决了所有单点的所有问题。

A distributed system is a system whose components are located on different networked computers, which communicate and coordinate their actions by passing messages to one another.[1] The components interact with one another in order to achieve a common goal. Three significant characteristics of distributed systems are: concurrency of components, lack of a global clock, and independent failure of components.[1] Examples of distributed systems vary from SOA-based systems to massively multiplayer online games to peer-to-peer applications.

个人认为高并发、高可扩、高性能以此为主推到出 并发性:一个大的任务可以划分为若干个子任务,分别在不同的主机上执行 可扩性:可弹性增降容 自治性:分布式系统中的各个节点都包含自己的处理机和内存,各自具有独立的处理数据的功能。通常,彼此在地位上是平等的,无主次之分,既能自治地进行工作,又能利用共享的通信线路来传送信息,协调任务处理 全局唯一性:分布式系统中必须存在一个单一的、全局的进程通信机制,使得任何一个进程都能与其他进程通信,并且不区分本地通信与远程通信。

What is the core idea of distribution?

分布式的核心理念是什么?

较于以上的了解,那么最为重要的为: 一致性:各主从机权限可不同、但需有一致性。建立维持自己的通信 容错性:拥有较为自主的稳定性,可能最大保障系统的正常运行

How to realize distributed?

如何实现分布式?

经过了解与探究,相信你已经抓住的有单机到多机(分布式)的命脉,那就是一致性,信号一致性。如果能想到这里,那么恭喜您,你和我一样还是比较需要去学习的。并没有去否定它的正确与否,仅仅是太过于笼统。 那么既然需要实现分布式,那么仅仅需要建立及维护此通信即可。保障它,你我就从单机的电脑插上了网线。是否有点feel呢?

分布式优缺点剖析

部分先后,仅从不同角度侧重概述。合而为一即可

闪光点: 稳定性:资源共享。若干不同的节点通过通信网络彼此互联,一个节点上的用户可以使用其他节点上的资源。 性能:加快计算速度 机会点 存在通信网络饱和或信息丢失和网络安全问题

RAFT

简介

Raft首先选举出一个server作为Leader,然后赋予它管理日志的全部责任。Leader从客户端接收日志条目,复制给其他server,并告诉他们什么时候可以安全的将日志条目应用到自己的状态机上。拥有一个Leader可以简化replicated log的管理。例如,leader可以决定将新的日志条目放在什么位置,而无需询问其他节点,数据总是简单的从leader流向其他节点。Leader可能失败或者断开连接,这种情况下会选出一个新的leader。 通过leader,Raft将一致性问题分解成三个相当独立的子问题:

  • Leader Election:当集群启动或者leader失效时必须选出一个新的leader。
  • Log Replication:leader必须接收客户端提交的日志,并将其复制到集群中的其他节点,强制其他节点的日志与leader一样。
  • Safety:最关键的安全点就是图3.2中的State Machine Safety Property。如果任何一个server已经在它的状态机apply了一条日志,其他的server不可能在相同的index处apply其他不同的日志条目。后面将会讲述raft如何实现这一点。

基础

一个Raft集群会包含数个server,5是一个典型值,可以容忍两个节点失效。在任何时候每个server都会处于Leader、Candidate、Follower三种状态中的一种。在正常情况下会只有一个leader,其他节点都是follower,follower是消极的,他们不会主动发出请求而仅仅对来自leader和candidate的请求作出回应。leader处理所有来自客户端的请求(如果客户端访问follower,会把请求重定向到follower)。Candidate状态用来选举出一个leader。 多个candidate想要成为leader,如果一个candidate赢得选举,它将会在剩余的term中作为leader。在一些情况下选票可能会被瓜分,导致没有leader产生,这个term将会以没有leader结束,一个新的term将会很快产生。Raft确保每个term至多有一个leader。Term在Raft中起到了逻辑时钟的作用,它可以帮助server检测过期信息比如过期的leader。每一个server都存储有current term字段,会自动随时间增加。当server间通信的时候,会交换current term,如果一个节点的current term比另一个小,它会自动将其更新为较大者。如果candidate或者leader发现了自己的term过期了,它会立刻转为follower状态。如果一个节点收到了一个含有过期的term的请求,它会拒绝该请求。 Raft节点之间通过RPC进行通信,基本的一致性算法仅仅需要两种RPC。RequestVote RPC由candidate在选举过程中发出,AppendEntries RPC由leader发出,用于复制日志和提供心跳。每一个请求类型都有对应的response,Raft假定request和response都可能会丢失,因此要求请求者有超时重试的能力。为了性能,RPC请求会并行发出,而且不保证RPC的到达顺序。

Leader election

Raft使用心跳机制来触发leader选举。当server启动的时候是处于follower状态,当它可以收到来自leader或者candidate的有效RPC请求时就会保持follower的状态。Leader发送周期性的心跳(不含日志的AppendEntries RPC)给所有的follower来确保自己的权威。如果一个follower一段时间(称为election timeout)没有收到消息,它就会假定leader失效并开始新的选举。 为了开始新一轮选举,follower会提高自己当前的term并转为candidate状态。它会先给自己投一票然后并行向集群中的其他server发出RequestVote RPC,candidate会保持这个状态,直到下面三种事情之一发生:

  1. 赢得选举。当candidate收到了集群中相同term的多数节点的赞成票时就会选举成功,每一个server在给定的term内至多只能投票给一个candidate,先到先得。收到多数节点的选票可以确保在一个term内至多只能有一个leader选出。一旦一个candidate赢得选举,它就会成为leader。它之后会发送心跳消息来建立自己的权威,并阻止新的选举。
  2. 另一个server被确定为leader。在等待投票的过程中,candidate可能收到来自其他server的AppendEntries RPC,声明它才是leader。如果RPC中的term大于等于candidate的current term,candidate就会认为这个leader是合法的并转为follower状态。如果RPC中的term比自己当前的小,将会拒绝这个请求并保持candidate状态。
  3. 没有获胜者产生,等待选举超时。candidate没有选举成功或者失败,如果许多follower同时变成candidate,选票就会被瓜分,形不成多数派。这种情况发生时,candidate将会超时并触发新一轮的选举,提高term并发送新的RequestVote RPC。然而如果不采取其他措施的话,选票将可能会被再次瓜分。

Raft使用随机选举超时来确保选票被瓜分的情况很少出现而且出现了也可以被很快解决。election timeout的值会在一个固定区间内随机的选取(比如150-300ms)。这使得在大部分情况下仅有一个server会超时,它将会在其他节点超时前赢得选举并发送心跳。candidate在发起选举前也会重置自己的随机election timeout,也可以帮助减少新的选举轮次内选票瓜分的情况

Log Replication

一旦一个leader被选举出来,它开始为客户端请求服务。每一个客户端请求都包含着一个待状态机执行的命令,leader会将这个命令作为新的一条日志追加到自己的日志中,然后并行向其他server发出AppendEntries RPC来复制日志。当日志被安全的复制之后,leader可以将日志apply到自己的状态机,并将执行结果返回给客户端。如果follower宕机或运行很慢,甚至丢包,leader会无限的重试RPC(即使已经将结果报告给了客户端),直到所有的follower最终都存储了相同的日志。 日志按下图的方式进行组织,每一条日志储存了一条命令和leader接收到该指令时的term序号。日志中的term序号可以用来检测不一致的情况,每一条日志也拥有一个整数索引用于定位。

总结

RAFT算法终究实现了什么?

  • 实现了信号一致性
  • 动态的Leader、Accessible or work 角色及节点管理,最大限度的保障了稳定性。
  • 他的核心为保障大多数都可用,即可正常运行。

寄语: 如果您是一位非常有经验的管理者或您有相对稳健的算法与数据结构基础, 是否与您所存在的了解的结构有所大多相似呢?-有效管理 直接上司管辖直属同事,算法中的BFS 在稍稍加入较为智能的选Leader,若大多数leader停止了工作,worker 将会进行相对应的“休息”,并没有接受到任务,简单的计算机选择做不如不做。毕竟Human nature is lazy

技术杂谈

Docker File 解析:

构建简史

编写一个dockerfile的文件,符合dockerfile的规范 docker build 执行,获得一个自定义的镜像 docker 运行 Docker执行docker file文件的大致流程

docker 从基础镜像运行一个容器 执行一条指令并对容器作出修改 执行类似docker commit的操作提交一个新的镜像层 docker 在基于刚提交的镜像运行一个新容器 执行docker 中的下一条指令知道所有指令都执行完成

Docker File基础知识:

每条保留字指令必须为大写字母且后面要跟随至少一个参数 指令从上到下、从左至右执行 ‘#’ :表示注释 每条指令都会创建一个新的镜像层,并对镜像进行提交

Docker File 体系结构:

保留字指令:

FROM:基础镜像,当前这个新的镜像是基于哪个镜像(scratch) MAINTAINER:镜像作者+邮箱 RUN:容器构建时所需要运行的命令 EXPOSE:当前容器对外暴露的端口号 WORKDIR:指定在创建容器后,终端默认登陆的进来工作目录,一个落脚点 ENV:用于构建镜像过程中设置环境变量 ADD: 拷贝加解压缩:将宿主机目录下的文件拷贝进镜像且add命令会自动处理rul和解压tar压缩包 COPY:将从构建上下文目录中<源路径>的文件/目录复制到新的一层的镜像内的<目标路径>位置

  • COPY src dest
  • COPY [“src”,”dest”]

VOLUME:容器数据卷,用于数据保存和持久化工作 CMD:

  • 指定一个容器启动时要运行的命令
  • DockerFile 中可以有多个CMD指令,但只有最后一个生效,CMD会被docker run 之后的参数替换!!!

ENTRYPOINT:

  • 指定一个容器启动时要运行的命令
  • 目的与CMD一样,都是在指定容器启动程序及参数
  • 不会被替换,被追加

ONBUILD:触发器 当构建一个被继承Docker File时运行的命令,父镜像在被子继承后父镜像的onbuild被触发

关键字详解

Dockerfile 分为四部分:

基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令。

基础镜像信息

FROM

1
2
# 格式
FROM <image> or FROM <image>:<tag>

如果在同一个Dockerfile中创建多个镜像时,可以使用多个 FROM 指令(每个镜像一次)

注意: Dockerfile每个保留字都会在docker容器中新建一层镜像层, 合理的减少镜像层以达节省资源的目的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# base
FROM python:3.7
RUN pip3 install requests
RUN pip3 install bs4
RUN pip3 install scrapy
RUN ...

# other
FROM python:3.7
RUN pip3 install requests bs64 scrapy # Python通过空格分隔,达到安装多个包的效果

# batter
# 外部建立所安装包
pip3 freeze >> requirements.txt
# 在Dockerfile 中
FROM python:3.7
RUN pip3 install -r requirements.txt

分析: 达到的效果虽相同,但所用资源却不同, base 所使用的资源约为other,batter的3/5

  • base 三层
  • other 一层
  • batter 一层
  1. 若所需要使用到的第三方库较少,建议使用other
  2. 在实际工程中用到的第三方库大多情况下都大于三个,方法二虽好.但所需导入的包一多,很可能出现单词拼写错误, 格式不符

维护者信息(非必须)

MAINTAINER

1
2
# 格式
MAINTAINR <name>

镜像操作指令(按需择选)

COPY

作用: 复制文件指令,从上下文 目录中复制或目录, 到容器中的指定路径 同等需求下建议使用COPY

1
2
3
4
5
6
# 格式
COPY <src> <dest>
COPY [--chown=<user>:<group>] <源路径1>... <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
# [--chown=<user>:<group>]:可选参数,用户改变复制到容器内文件的拥有者和属组。
# 复制本地主机的 `<src>`(为 Dockerfile 所在目录的相对路径)到容器中的 `<dest>`

<源路径>:源文件或者源目录,这里可以是通配符表达式,其通配符规则要满足 Go 的 filepath.Match 规则。

1
2
COPY hom* dir/
COPY hom?.txt dir/

<目标路径>:容器内的指定路径,该路径不用事先建好,路径不存在的话,会自动创建。

ADD

作用: 复制文件指令,从上下文 目录中复制或目录, 到容器中的指定路径 同等需求下建议使用COPY

1
2
3
4
5
# 格式 
ADD <src> <dest>`

# 该命令将复制指定的 `<src>` 到容器中的 `<dest>`。
# 其中 `<src>` 可以是Dockerfile所在目录的一个相对路径;也可以是一个 URL;还可以是一个 tar 文件(自动解压为目录)。
  • ADD 的优点:在执行 <源文件> 为 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,会自动复制并解压到 <目标路径>。
  • ADD 的缺点:在不解压的前提下,无法复制 tar 压缩文件。会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。具体是否使用,可以根据是否需要自动解压来决定

小结:

  • ADD 与 COPY 功能无明显差异, 但针对性不同
  • 当使用本地文件为源目录时,建议使用Copy
  • 当需使用压缩包中文件时构建时,建议使用Copy

RUN

作用: 用于在容器内执行命令

1
2
3
# 格式
RUN <command> [option] # 相当于shell格式
RUN ["command", "option1", 'option2'] # 相当于 exec 格式

WORKDIR

作用:指定工作目录。用 WORKDIR 指定的工作目录,会在构建镜像的每一层中都存在。(WORKDIR 指定的工作(主)目录,必须是提前创建好)。 docker build 构建镜像过程中的,每一个 RUN 命令都是新建的一层。只有通过 WORKDIR 创建的目录才会一直存在。

1
2
# 格式
WORKDIR <dir>

USE

作用:用于指定将使用命令的用户和用户组 此处只是切换后续命令执行的用户(用户和用户组必须提前已经存在)。 由于docker 无设置,默认需在root权限下运行 正所谓权限越大能力越大,若被入侵则造成损失较于其他权限为最大 安全与灵活性 二者相对斟酌

1
2
# 格式
USER <username>[:<usergroup>]

EXPOSE

作用:仅声明端口

  • 方便配置映射
  • 在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。
1
2
# 格式
EXPORT <port1> [<prot2> <prot3> ...]

执行指令

CMD

作用: 类似于 RUN命令,但运行处不同

  • RUN 在Docker build 前运行
  • CMD 在docker run 后运行

注意:

  • 当 Dockerfile 中存在多个 CMD 指令,仅最后一个生效。
  • CMD 指令指定的程序可被 docker run 命令行参数中指定要运行的程序所覆盖。

当使用自定制镜像时,大致流程为

  • 编写业务代码
  • 构建定制镜像(docker build)
  • docker 中运行业务 (Docker run)
1
2
3
4
5
# 格式
CMD <ShellCommand1 [option]> [&&ShellCommand2[option] ]
CMD ["<可执行命令或文件>", "<option1>", "<option2>", ... ]
# 此写法为保留字 ENTRYPOINT 指令指定的程序提供默认参数
CMD ["<command1>", "<command2>", "<command3>", ...]

ENTRYPOINT

作用: 功能与CMD相似,但不会被docker run 后的指定参数所覆盖, 命令行参数会被当作参数送给 ENTRYPOINT 指令指定的程序。 但是当docker run 使用了 —entrypoint 选项此选项的参数,可当作要运行的程序覆盖 ENTRYPOINT 指令指定的程序。 ENTRYPOINT CMD对比 同: 多个 指令,仅最后一个生效。 异: ENTRYPOINT选项的参数可当作要运行的程序覆盖 ENTRYPOINT 指令指定的程序。

1
2
# 格式
ENTRYPOINT ["<可执行命令或文件>","option1", "option2"]

ENV

作用:配置容器内的环境变量,且保存,可以被后续 指令使用

1
2
3
# 格式
ENV <Key> <Values>
ENV <Key1>=<Values1>, <Key2>=<Values2> ...

ARG

作用:配置容器内的环境变量,且保存,可以被后续 指令使用(与ENV功能相似) 不同:作用域不一样。ARG 设置的环境变量仅对 Dockerfile 内有效 也就是说只有 docker build 的过程中有效,构建好的镜像内不存在此环境变量。

1
构建命令 docker build 中可以用 --build-arg <参数名>=<值> 来覆盖。
1
2
3
# 格式
ARG <key>[=默认值], <Value>
# 若不写Value 则Build时为自己写的默认值

VOLUME

作用:定义匿名数据卷。在启动容器时忘记挂载数据卷,会自动挂载到匿名卷。

  • 避免重要的数据,因容器重启而丢失
  • 避免容器不断变大
1
2
3
# 格式:
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>

在启动容器 docker run 的时候,我们可以通过 -v 参数修改挂载点。

HEALTHCHECK

作用: 用于指定某个程序或者指令来监控 docker 容器服务的运行状态。

格式:

1
2
3
HEALTHCHECK [option] CMD <command> #设置检查容器健康状况的命令
HEALTHCHECK NONE # 如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令
HEALTHCHECK [option] CMD <command> # CMD 后面跟随的命令使用

ONBUILD

作用: 用于延迟构建命令的执行。 先构建一个父类镜像(ONBUILD 在父类中,但不立即执行),后子类继承此父类镜像(执行父类的ONBUILD 命令) 本次并不执行,当镜像调用它时,将执行父类中ONBUILD命令

1
2
# 格式
ONBUILD <其它指令>

技术杂谈

Docker容器数据卷

Docker理念:

  • 将运用与运行的环境打包形成容器运行,运行可以伴随着容器,但由于对数据要求希望是持久化的
  • 容器之间希望可以共享数据

一、Docker容器数据卷是什么?

  • docker容器产生的数据,如果不通过docker commit生成新的镜像,使数据做为镜像的一部分保存下来,那么删除容器之后,数据也随之被删除。为了能保存数据在docker中,我们使用容器卷。
  • 好比从电脑(docker)中拷贝数据(使用的U盘-容器卷)

二、Docker容器数据卷能干什么?

  • 数据的持久化
  • 容器间继承+共享数据

特点:

  • 容器之间共享过重用数据
  • 卷中更改可之间生效
  • 数据卷中的更改不会在镜像的更新中
  • 数据中的更改不会包含在镜像的更新中
  • 数据卷的生命周期一直持续到没有容器使用止

三、Docker容器数据卷

容器内添加:

1
2
docker run -it -v /[宿主机绝对路径目录]:[/容器内目录] [container_ID]
docker run -it -v /myDataVolume:/dataVolumeContainer [镜像名]

权限报错处理:

1
docker run -it -v /myDataVolume:/dataVolumeContainer --privileged=true [镜像名]

检测是否数据卷是否挂载成功

1
docker inspect [container_ID]

容器停止后,主机修改后数据是否同步 可以!但需为同一个容器!!![The same container_id]

1
2
3
使用docker ps -l 查看运行过的容器信息
# 查看运行容器信息
docker ps -l

命令(带权限):容器中只读,不可修改 docker run -it -v /宿主机绝对路径:/容器内目录:ro 镜像名

dockerfile 添加

javaEE:hello.java —-> hello.class Docker: images ===》 DockerFile

  1. 新建mydocker文件夹并进入
  2. 在dockerfile中使用volume指令来给镜像添加一个或多个数据卷
    • volume[“/dataVolumeContainer”,”/dataVolumeContainer2”,”/dataVolumeContainer3”]
  3. File构建

    1
    2
    3
    4
    5
    # volume test
    FROM centos
    VOLUME ["/dataVolumeContainer1","/dataVolumeContaine2"]
    CMD echo "finished,----sucess1"
    CMD /bin/bash
  4. build后生成镜像

    1
    2
    3
    4
    docker build -f /路径/文件名 -t 容器名:TAG . 
    -f: file-->指定为文件
    -t:为容器重新分配一个为输入终端;
    . : 分布执行file中命令

四、Docker容器数据卷容器

命名的容器挂载数据卷,其他容器通过挂载这个(父容器)实现数据共享,挂载数据卷的容器称之为数据卷容器 容器间传递共享(volumes-from)

dc01(主) 删除后 dc02(子1) dc03(子2)不受影响,dc02与dc3 继续传输

结论:容器之间配置信息的传递,数据卷的生命周期一直持续到没有容器使用为止

技术杂谈

搭建个人Leanote云笔记

Leanote是一款在线的云笔记应用,有如下特点:

  • 支持网页、PC、手机APP客户端和微信版,随时记录,方便分享,支持语音,图片输入。
  • 代码高亮,涵盖所有主流语言的代码高亮,随心所欲在Leanote里写代码,记知识。
  • Markdown 编辑器,实时同步预览。
  • 专业数学公式编辑,像Word和Latex能编辑数学公式。
  • 支持创建思维脑图,将散乱的想法以树状信息分层展示。
  • 详细历史纪录,每次保存都在后端备份,轻松查找,一键恢复。
  • 实时同步云端。

简介

基于Linux + Mongo + Leanote 快速搭建个人云笔记

安装MongoDB

MongoDB是一个基于分布式文件存储的高性能数据库,介于关系数据库和非关系数据库之间,它支持的数据结构非常松散是类似于json和bson格式,因此可以存储比较复杂的数据类型。Mongo最大的特点是它支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。 Leanote云笔记使用MongoDB作为后端数据库,按照以下步骤按照MongoDB数据库。

1
2
3
4
5
6
# install MongoDB
yum -y install mongodb mongodb-server.x86_64 mariadb-devel.i686
# start MongoDB service
systemctl start mongod
# Verify mongodb running state
systemctl status mongod

安装Leanote

1
2
3
4
5
6
7
8
# Download Leanote installation package
wget https://nchc.dl.sourceforge.net/project/leanote-bin/2.6.1/leanote-linux-amd64-v2.6.1.bin.tar.gz
# Unzip
tar -zxvf leanote-linux-amd64-v2.6.1.bin.tar.gz
# setting leanote
vim leanote/conf/app.conf
# change app.secret
app.secret = Self configuration

配置服务

If the following message appears

Failed global initialization: BadValue Invalid or no user locale set. Please ensure LANG and/or LC_*

Please configure the environment variables as follows

1
export LC_ALL=C
1
2
3
4
# init MongoDB
mongorestore -h localhost -d leanote --dir /root/leanote/mongodb_backup/leanote_install_data/
# Start service
nohup bash /root/leanote/bin/run.sh > /root/leanote/run.log 2>&1

访问笔记

在浏览器中访问服务器弹性地址:端口(默认9000)

1
2
3
# 默认账号
username: admin
password: abc123