Elasticsearch常用增删改查记录

最近使用Elasticsearch来做异常监控的存储,写了不少ES的索引操作,以及对数据的增删改查操作,记录一下,以备不时之需。

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790

# 列出所有索引
GET _cat/indices

# 查看indices
GET /_cat/indices/jz-fe*?v&s=index

# 查看索引的文档总数
GET jz-fe-http-log/_count
# 删除索引
# DELETE /jz-fe-http-log
# 查看索引相关信息
GET nginx-log-bjdaojiacom-2019.07.20
GET jz-fe-http-log
# 创建索引,禁止自动添加类型字段
PUT /jz-fe-http-log
{
"mappings": {
"dynamic": "strict",
"properties":{
"gitGroup":{"type":"keyword"},
"projectName":{"type":"keyword"},
"level":{"type":"keyword"},
"code":{"type":"keyword"},
"href":{"type":"text"},
"url":{
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword"
}
}
},
"method":{"type":"keyword"},
"param":{"type":"text"},
"response":{"type":"text"},
"message":{
"type":"text",
"fields" : {
"keyword" : {
"type" : "keyword"
}
}},
"stack":{"type":"text"},
"clientDate":{"type":"date","format":"yyyy-MM-dd HH:mm:ss"},
"serverDate":{"type":"date","format":"yyyy-MM-dd HH:mm:ss"},
"uid": {"type":"keyword"},
"phone": {"type":"keyword"},
"os":{"type":"text"},
"platform":{"type":"text"},
"browser":{"type":"keyword"},
"version":{"type":"keyword"},
"userAgent":{"type":"text"},
"status":{"type":"integer"},
"closeDate":{"type":"date","format":"yyyy-MM-dd HH:mm:ss"}
}
}
}

# 更新索引-增加字段
PUT jz-fe-http-log/_mapping
{
"dynamic": "strict",
"properties": {
"status":{"type":"integer"},
"closeDate":{"type":"date","format":"yyyy-MM-dd HH:mm:ss"}
}
}

# 更新索引-修改字段
PUT jz-fe-http-log/_mapping
{
"properties": {
"url":{
"type":"text",
"fields" : {
"keyword" : {
"type" : "keyword"
}
}}
}
}

# 增加数据
POST /jz-fe-http-log/_doc
{
"os":"OS X",
"platform":"iPhone",
"browser":"Safari",
"version":"11.0",
"clientDate":"2019-08-23 18:54:03"
}

# 指定key查询
GET /jz-fe-http-log/_search
{
"query": {
"match": {
"_id": "BYG_SG0BW-qMNGXgwzPg"
}
}
}

# 按文档ID更新指定字段数据
POST /jz-fe-http-log/_doc/R9Wm_mwBAl5tA0U3Hq4l/_update
{
"doc" : {
"status": 1
}
}

# id删除数据
DELETE /jz-fe-http-log/_doc/l0_772wBAl5tA0U3dpBH
# 删除指定查询数据 - 前缀匹配
# POST /jz-fe-http-log/_delete_by_query
POST /jz-fe-http-log/_delete_by_query
{
"size":5,
"query": {
"regexp": { "projectName": "daily-clean-v2.+" }
}
}

POST /jz-fe-http-log/_delete_by_query
{
"size":5,
"query": {
"match": { "projectName": "<%- 909275197+844400363 %>" }
}
}

# 异常总数
GET /jz-fe-http-log/_search
{
"size":0,
"query": {
"match_all": {}
}
}

# 已处理异常总数
GET /jz-fe-http-log/_search
{
"size":0,
"query": {
"match": {
"status": 1
}
}
}

# 当天异常总数
GET /jz-fe-http-log/_search
{
"size":0,
"query": {
"match": {
"serverDate": "now/1d"
}
}
}

# 今日异常数
POST /jz-fe-http-log/_search
{
"size":0,
"query": {
"bool": {
"must": [
{ "match": { "serverDate": "now/1d" }},
{ "match": { "status": "1" }}
]
}
}
}


# 按照项目名称分组
GET jz-fe-http-log/_search
{
"size":0,
"query": {
"bool": {
"must": [
{
"range": {
"serverDate": {
"gte": "2019-10-30 14:57:00",
"lte": "2019-10-31 14:57:00"
}
}
}
]
}
},
"aggs": {
"group_by_projectName": {
"terms": {
"size": 50,
"field": "projectName"
}
}
}
}

# 按照git group+项目名称分组
GET jz-fe-http-log/_search
{
"size":0,
"aggs": {
"group_by_gitGroup": {
"terms": {
"size": 50,
"field": "gitGroup"
},
"aggs": {
"group_by_projectName": {
"terms": {
"size": 50,
"field": "projectName"
}
}
}
}
}
}

# 指定项目下,错误等级分组
GET jz-fe-http-log/_search
{
"size":0,
"query": {
"match": {
"projectName": "daily-clean"
}
},
"aggs": {
"group_by_projectName": {
"terms": {
"field": "level"
}
}
}
}

# 按天分组、计数
GET jz-fe-http-log/_search
{
"size":0,
"aggs": {
"grouy_by_day": {
"date_histogram": {
"field": "serverDate",
"interval": "day",
"format" : "yyyy-MM-dd"
}
}
}
}

# 最近一周所有问题增长趋势
GET jz-fe-http-log/_search
{
"size":0,
"track_total_hits": 100000,
"query": {
"bool": {
"must": [
{
"range": {
"serverDate": {
"gte": "2019-10-30 14:57:00",
"lte": "2019-10-31 14:57:00"
}
}
}
]
}
},
"aggs": {
"grouy_by_hour": {
"date_histogram": {
"field": "serverDate",
"interval": "hour",
"format" : "yyyy-MM-dd HH:mm:ss"
}
}
}
}

# 上一周指定项目的问题增长趋势
GET jz-fe-http-log/_search
{
"size":1,
"query": {
"bool": {
"must": [
{ "match": { "projectName": "daily-clean" }},
{
"range": {
"serverDate": {
"gte": "2019-10-30 14:57:00",
"lte": "2019-10-31 14:57:00"
}
}
}
]
}
},
"aggs": {
"grouy_by_hour": {
"date_histogram": {
"field": "serverDate",
"min_doc_count": 0,
"interval": "hour",
"format" : "yyyy-MM-dd HH:mm:ss"
}
}
}
}


# 指定项目,按message分组
GET jz-fe-http-log/_search
{
"size":0,
"query": {
"bool": {
"must": [
{ "match": { "projectName": "csworker" }}
]
}
},
"aggs": {
"grouy_by_message": {
"terms": {
"size": 25,
"field": "message.keyword"
}
}
}
}

# 按指定项目中的message分页查询
POST /jz-fe-http-log/_search
{
"size":2,
"from": 0,
"query": {
"bool": {
"must": [
{ "match": { "projectName": "daily-clean" }},
{ "match": { "message.keyword": "无可用商家" }}
]
}
}
}

# 指定项目+message,按url分组,查询出指定错误的url排行
POST /jz-fe-http-log/_search
{
"size":0,
"from": 0,
"query": {
"bool": {
"must": [
{ "match": { "projectName": "daily-clean" }},
{ "match": { "message.keyword": "timeout of 5000ms exceeded" }}
]
}
},
"aggs": {
"group_by_url": {
"terms": {
"field": "url.keyword"
}
}
}
}


# 按时间降序
POST /jz-fe-http-log/_search
{
"size":1,
"query": {
"bool": {
"must": [
{ "match": { "projectName": "daily-clean" }},
{ "match": { "message.keyword": "无可用商家" }}
]
}
},
"sort": [
{"serverDate": "desc"} ,
{"_id": "asc"}
]
}

# 按时间降序-searchAfter
POST /jz-fe-http-log/_search
{
"size":1,
"query": {
"bool": {
"must": [
{ "match": { "projectName": "daily-clean" }},
{ "match": { "message.keyword": "无可用商家" }}
]
}
},
"search_after":[
1569489763000,
"NP4pa20BFerq1YvUydLx"
],
"sort": [
{"serverDate": "desc"} ,
{"_id": "asc"}
]
}

# 指定时间段,按项目分组
GET jz-fe-http-log/_search
{
"size":0,
"query": {
"range": {
"serverDate": {
"gt": "2019-09-24 09:00:00",
"lt": "2019-09-24 10:00:00",
"format": "yyyy-MM-dd HH:mm:ss"
}
}
},
"aggs": {
"grouy_by_project": {
"terms": {
"field": "projectName"
}
}
}
}

# 查询最近一小时的数据,按项目分组
GET jz-fe-http-log/_search
{
"size":0,
"query": {
"bool": {
"must": [
{
"range": {
"serverDate": {
"gte": "2019-10-31 12:57:00",
"lte": "2019-10-31 13:57:00"
}
}
}
]
}
},
"aggs": {
"grouy_by_project": {
"terms": {
"size": 100,
"field": "projectName"
}
}
}
}



# 异常查询
GET jz-fe-http-log/_search
{
"size":30,
"query": {
"bool": {
"must": [

{
"range": {
"serverDate": {
"gte": "2020-01-18 17:00:00",
"lte": "2020-01-18 17:30:00"
}
}
}
]
}
},
"aggs": {
"grouy_by_project": {
"terms": {
"size": 100,
"field": "projectName"
}
}
}
}

GET jz-fe-http-log/_search
{
"size":0,
"query": {
"bool": {
"must": [
{ "match": { "projectName": "daily-clean-v2" }},
{
"range": {
"serverDate": {
"gte": "2020-01-18 07:00:00",
"lte": "2020-01-18 13:00:00"
}
}
}
]
}
},
"aggs": {
"grouy_by_message": {
"terms": {
"size": 25,
"field": "message.keyword"
}
}
}
}

GET jz-fe-http-log/_search
{
"size":0,
"query": {
"bool": {
"must": [
{ "match": { "projectName": "daily-clean-v2" }},
{
"range": {
"serverDate": {
"gte": "2020-01-18 07:00:00",
"lte": "2020-01-18 13:00:00"
}
}
}
]
}
},
"aggs": {
"group_by_url": {
"terms": {
"field": "url.keyword"
}
}
}
}




# ----------------------------------------------------------------------------
# 性能监控相关
# ----------------------------------------------------------------------------
# 查看索引相关信息
GET jz-fe-performance-log
# 查看索引的文档总数
GET jz-fe-performance-log/_count
# 删除索引
DELETE /jz-fe-performance-log
# 性能监控-创建索引,禁止自动添加类型字段
PUT /jz-fe-performance-log
{
"mappings": {
"dynamic": "strict",
"properties":{
"groupName":{"type":"keyword"},
"projectName":{"type":"keyword"},
"href":{"type":"keyword"},
"clientDate":{"type":"date","format":"yyyy-MM-dd HH:mm:ss"},
"serverDate":{"type":"date","format":"yyyy-MM-dd HH:mm:ss"},
"appId":{"type":"keyword"},
"hmsr":{"type":"keyword"},
"znsr":{"type":"keyword"},
"hmpl":{"type":"keyword"},
"unloadTime":{"type":"integer"},
"redirectTime":{"type":"integer"},
"appCacheTime":{"type":"integer"},
"dnsTime":{"type":"integer"},
"tcpTime":{"type":"integer"},
"requestTime":{"type":"integer"},
"responseTime":{"type":"integer"},
"analysisTime":{"type":"integer"},
"loadEventTime":{"type":"integer"},
"connectTime":{"type":"integer"},
"resourceTime":{"type":"integer"},
"domReadyTime":{"type":"integer"},
"TTFBTime":{"type":"integer"},
"TTSRTime":{"type":"integer"},
"TTDCTime":{"type":"integer"},
"TTFLTime":{"type":"integer"},
"uid":{"type":"keyword"},
"phone":{"type":"keyword"},
"platform":{"type":"keyword"},
"os":{"type":"keyword"},
"browser":{"type":"keyword"},
"version":{"type":"keyword"},
"userAgent":{"type":"text"},
"ip":{"type":"keyword"},
"networkType":{"type":"keyword"},
"ISP":{"type":"keyword"},
"region":{"type":"keyword"}
}
}
}

# 查询索引下的所有数据
GET /jz-fe-performance-log/_search
{
"query": {
"match_all": {}
}
}

# 查看索引详情
GET /_cat/indices/jz-fe*?v&s=index
# 删除后释放空间
POST jz-fe-performance-log/_forcemerge
# 删除指定时间段的数据
POST jz-fe-performance-log/_delete_by_query?scroll_size=10000&slices=10
{
"query": {
"bool": {
"must": [
{
"range": {
"serverDate": {
"lte": "2020-01-01 00:00:00"
}
}
}
]
}
}
}

# 查询出指定时间段项目的平均打开时间
GET jz-fe-performance-log/_search
{
"size":0,
"track_total_hits": 10000000,
"query": {
"bool": {
"must": [
{
"range": {
"serverDate": {
"gte": "2020-01-10 12:00:00",
"lte": "2020-01-18 13:00:00"
}
}
}
]
}
},
"aggs":{
"projectAvgTime":{
"terms":{
"size": 50,
"script": "'【'+doc['groupName'].value+'】'+doc['projectName'].value",
"order": {
"avgTime": "desc"
}
},
"aggs":{
"avgTime":{
"avg":{
"field":"TTFLTime"
}
},
"minTime":{
"min":{
"field":"TTFLTime"
}
},
"maxTime":{
"max":{
"field":"TTFLTime"
}
}
}
}
}
}

# 秒开率计算
POST jz-fe-performance-log/_search
{
"size": 0,
"track_total_hits": 10000000,
"query": {
"bool": {
"must": [
{ "range": { "TTFLTime": { "lte": 3000 } } },
{ "match": { "projectName": "daily-clean-v2" }},
{
"range": {
"serverDate": {
"gte": "2020-01-10 12:00:00",
"lte": "2020-01-18 13:00:00"
}
}
}
]
}
}
}

# 平均首字节时间
GET jz-fe-performance-log/_search
{
"size":0,
"track_total_hits": 10000000,
"query": {
"bool": {
"must": [
{
"range": {
"serverDate": {
"gte": "2020-01-10 12:00:00",
"lte": "2020-01-18 13:00:00"
}
}
}
]
}
},
"aggs":{
"avgTime":{
"avg":{
"field":"TTFLTime"
}
},
"minTime":{
"min":{
"field":"TTFLTime"
}
},
"maxTime":{
"max":{
"field":"TTFLTime"
}
}
}
}

# 性能柱状图
GET jz-fe-performance-log/_search
{
"size":0,
"track_total_hits": 10000000,
"query": {
"bool": {
"must": [
{ "range": { "TTFLTime": { "gte": 250, "lte": 349 } } },
{
"range": {
"serverDate": {
"gte": "2020-02-05 16:19:19",
"lte": "2020-02-06 16:19:19"
}
}
}
]
}
},
"aggs":{
"chartData":{
"terms":{
"size": 1000,
"field": "TTFLTime",
"script": "Math.round(_value/100)",
"order": {
"_key": "asc"
}
}
}
}
}


留言

欢迎交流想法。留言会通过 GitHub Issues 保存,首次使用需要登录 GitHub。

Elasticsearch7.x安装与配置

下载

1.下载地址:https://www.elastic.co/cn/start
复制下载链接

1
2
3
4
# 下载资源
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.3.0-linux-x86_64.tar.gz
# 解压资源
tar -xf elasticsearch-7.3.0-linux-x86_64.tar.gz

运行

1
2
3
4
5
6
cd elasticsearch-7.3.0-linux-x86_64
# 显示启动elk
./bin/elasticsearch
# 后台启动
./bin/elasticsearch -d

其它配置

跨域配置

当使用elasticsearch-head插件访问elk时,需要设置允许跨域访问

1
2
3
4
vim config/elasticsearch.yml
# 新增如下内容
http.cors.enabled: true
http.cors.allow-origin: "*"

允许外部网络(非本地)访问9200端口

默认情况下,启动elk服务后,其它服务器是无法访问elk服务的(telnet不通),只允许本地访问。

需要做如下配置:

1
2
3
4
vim config/elasticsearch.yml
# 新增如下内容
network.host: 0.0.0.0
http.port: 9200

此时,启动会报如下两个错误:

1
2
3
ERROR: [2] bootstrap checks failed
[1]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
[2]: the default discovery settings are unsuitable for production use; at least one of [discovery.seed_hosts, discovery.seed_providers, cluster.initial_master_nodes] must be configured

对于第一个配置,root用户权限下,增加如下配置:

1
2
3
4
vi /etc/sysctl.conf
# 新增如下内容
vm.max_map_count=262144
sysctl -p

对于第二个错误,增加如下配置:

1
2
3
vim config/elasticsearch.yml
# 新增如下内容
cluster.initial_master_nodes: [“node-1”]

作者公众号:

留言

欢迎交流想法。留言会通过 GitHub Issues 保存,首次使用需要登录 GitHub。

CodeReview过程中关于JS代码性能的随想整理

问题

  团队中做code review有一段时间了,最近一直在思考一个问题,抛开业务逻辑,单纯从代码层面如何评价一段代码的好坏?

  好和坏都是相对的,一段不那么好的代码经过优化之后,如何标准化的给出重构前后的差异呢?

  我们所有的代码都跑在计算机上,计算机的核心是CPU和内存。从这个角度来看,效率高的代码应当占用更少的CPU时间,更少的内存空间。

  因此,问题就演变为优化一段代码,到底优化了多少CPU的使用以及内存空间的使用?

CPU-时间复杂度

  在数据结构与算法中,常用大O来表示算法的时间复杂度,常见的时间复杂度如下所示:(来源《算法》第四版)

  时间复杂度这个东西,是描述一个算法在问题规模不断增大时对应的时间增长曲线。所以,这些增长数量级并不是一个准确的性能评价,可以理解为一个近似值,时间的增长近似于logN、NlogN的曲线。如下图所示:

  上面是关于时间复杂度的解释,下面通过具体样例来看看代码的时间复杂度

代码一:

1
2
3
4
5
6
7
8
(function count(arr=[1,2,3,4,5,6,7,8,9,10]){
let num = 0
for(let i=0;i<arr.length;i++){
let item = arr[i]
num = num + item
}
return num
})()

  这是一段求数组中数字总和的代码,我们粗略估计上述代码在CPU中表达式运算的时间都是一样的,计为avg_time,那么我们来算一下上面的代码需要多少个avg_time.

  首先从第二行开始,表达式赋值计为1个avg_time;代码的3、4、5行分别要运行10次,其中第三行比较特殊,每次运行需要计算arr.length以及i++,所以这里需要(2+1+1)*10 个avg_time;总共就是(2+1+1)*10+1=41个avg_time

  接着,我们来对上面的代码优化一番,如下所示:
代码二

1
2
3
4
5
6
7
8
(function count(arr=[1,2,3,4,5,6,7,8,9,10]){
let num = 0
let len = arr.length
while(len--){
num = num + arr[len]
}
return num
})()

  不难算出,优化后的代码只耗费了1+1+(1+1)*10=22个avg_time,代码二相对于代码一,节约了41-22=19个avg_time,代码性能提升19/41=46.3%!

如何写出低时间复杂度的代码?

1.灵活使用break、continue、return

  这三个关键字一般用在减少循环次数,达到目的,立即退出。如下所示:

1
2
3
4
5
6
7
8
9
10
(function check(arr=[1,2,3],target=2){
let len = arr.length
while(len--){
if(arr[len]===target){
// 不再继续后续循环
return len
}
}
return -1
})()

2.空间换时间

常见的做法是利用缓存,把上次的计算结果存起来,避免重复计算。

3.更优的数据结构与算法

根据不同的情况选择合适的数据结构与算法,例如,如果需要频繁的从一组数据中通过关键key查询出数据,如果要从json对象和数组中选择,那么可以优先考虑使用json对象来避免数组的遍历查询。

内存-空间复杂度

  评价一段代码,除了看它执行需要多少时间,还需要看看需要多少空间,谈到代码的空间占用,必须就得知道JS的内存管理

  JS的内存管理分为三部分:

  • 内存分配。
      这里包含包含代码本身以及静态数据与动态数据所需要的内存,其中代码本身与静态数据会分配在stack上,可变的动态数据会分配在heap上

  • 使用分配的内存。

  • 内存回收。

这里,放一张JS Runtime的图

静态内存分配

  是指stack中内存的分配,基础数据类型的数据就放在stack中。另外,stack是有固定大小的,超过stack的长度,就会报错,所以必须得节约着用。

爆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 故意来一次爆栈体验
function foo(){
foo()
}
foo()
// 结果
VM201:1 Uncaught RangeError: Maximum call stack size exceeded
at foo (<anonymous>:1:13)
at foo (<anonymous>:2:3)
at foo (<anonymous>:2:3)
at foo (<anonymous>:2:3)
at foo (<anonymous>:2:3)
at foo (<anonymous>:2:3)
at foo (<anonymous>:2:3)
at foo (<anonymous>:2:3)
at foo (<anonymous>:2:3)
at foo (<anonymous>:2:3)

  我们是怎么达到爆栈目的的呢?因为所有的函数调用,在内存中都存在一个函数调用栈,我们不断无结束条件的递归调用,最终撑破了stack。

如图所示:

函数调用栈

可能你会问怎么证明函数调用栈的存在呢?请看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function second() {
throw new Error('function call stack');
}
function first() {
second();
}
function start() {
first();
}
start();
// 结果如下
VM266:2 Uncaught Error: function call stack
at second (<anonymous>:2:11)
at first (<anonymous>:5:5)
at start (<anonymous>:8:5)
at <anonymous>:10:1

  从上面的运行结果可以看出函数调用栈的顺序,start先入栈,接着first,最后second;打印顺序为首选打印second,最后打印start;满足栈的先进后出的数据结构特性。

内存占用

  了解上面知识点的核心目的还是在于指导我们写出更优的代码,我们知道基本数据类型都放在栈中,对象都放在堆中。另外,通过《JavaScript权威指南》第六版第三章可以知道,js中的数字都是双精度类型,占64位8个字节的空间,字符占16位2个字节的空间。

  有了这个知识,我们就可以估算出我们的代码大致占用了多少内存空间。

  这些毕竟都是理论知识,不禁要怀疑一下,的确是这样的吗?下面我们利用爆栈的原理,通过代码实际瞧瞧

1
2
3
4
5
6
7
8
9
10
11
let count = 0
try{
function foo() {
count++
foo()
}
foo()
}finally{
console.log(count)
}
// 最终的打印结果为:15662

我们知道一个数字占8个字节,栈的大小固定;稍微变更一下代码

1
2
3
4
5
6
7
8
9
10
11
12
let count = 0
try{
function foo() {
let local = 58 //数字,占8个字节
count++
foo()
}
foo()
}finally{
console.log(count)
}
// 最终的打印结果为:13922

那么我们可以利用如下方法算一下栈的总大小

1
2
3
4
5
6
N = 栈中单个元素的大小
15662 * N = 13922 * (N + 8) // 两次函数调用,栈的总大小相等
(15662 - 13922) * N = 13922 * 8
1740 * N = 111376
N = 111376 / 1740 = 64 bytes
Total stack size = 15662 * 64 = 1002368 = 0.956 MB

注:不通环境可能结果不太一样

接下来,我们来确定一下数字类型是否占8个字节空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let count = 0
try{
function foo() {
//数字,占8个字节,这里就占16个字节
let local = 58
let local2 = 85
count++
foo()
}
foo()
}finally{
console.log(count)
}
// 最终的打印结果为:12530

计算一下Number的内存占用大小

1
2
3
4
// 总的栈内存空间/栈中元素数量 = 单个栈元素大小
1002368/12530 = 80
// 对比不带任何额外变量的代码,单个栈元素大小是64,这里新增两个16,加起来正好为80
80 = 64+8+8

经实际验证,在Chrome、Safari、Node环境下,不论变量的值是什么类型,在stack中都占8个字节。对于字符串貌似跟预期不太一样,不论多长的字符串实践表明在stack中都占8个字节,怀疑浏览器默认把字符串转换为了对象,最终占用heap空间

动态内存分配

  是指heap中内存的分配,所有对象都放在heap中,stack中只放对象的引用。

这里有一篇数组占用多少内存空间的文章:How much memory do JavaScript arrays take up in Chrome?

如何写出低内存占用的代码?

  低内存占用,从静态内存分配方面可以考虑,尽量少的使用基础类型变量;从动态内存分配的角度,让代码更简洁、不要毫无节制的new一个对象、少在对象放东西;

下面是一些小技巧:
1.三目运算符

1
2
3
4
5
6
7
8
// 条件赋值
if(a===1){
b = 'aa'
}else{
b = 'bb'
}
// 可简化为
b = a===1 ? 'aa' : 'bb'

2.直接返回结果

1
2
3
4
5
6
7
if(a===1){
return true
}else{
return false
}
// 可简化为
return a===1

一时半会儿想不到好的样例,上面的样例至少节约了代码的空间占用!……欢迎评论补充……

内存回收

  我的理解是,当函数调用栈为空时,占用的占内存随之清空;只有堆内存中的数据才需要通过垃圾回收机制来回收。

常见的垃圾回收算法如下:

  • 引用计数
    对没有对象的引用计数,如果没有任何外部引用时,则清除该对象;引用计数算法有一个弊端就是无法清除循坏依赖的对象

  • 标记清除:
    每次回收,从根对象开始遍历,能遍历到的对象则记为可用,不能遍历到的对象则为需要垃圾回收的对象。此种算法能够解决对象循环依赖的问题。

  • 综合算法:
    实际上垃圾回收是一个很复杂的过程,垃圾回收器会根据内存的不通情况采取不同的垃圾回收算法,来实现效率的最大化。

这里有一篇垃圾回收的文章:A tour of V8: Garbage Collection 已经被翻译为了中文,点进去就知道了。

如何避免内存溢出?

  从上面的垃圾回收机制不难看出,当某些情况内存无法被回收且不断增加时,内存溢出就会产生。下面是几种常见的会有内存溢出风险的代码。

1.控制全局变量
从垃圾回收的原理我们可以知道,全局变量肯定是不会被回收的。所以我们应当尽量把数据绑定到全局变量上,更应该避免通过用户操作持续的增加全局变量数据的大小。
另外还需要特别注意意外的全局变量产生,例如:

1
2
3
4
5
6
function foo(arg) {
a = "some text";
this.b = "some text";
}
// 会在window对象上新增a,b属性
foo()

2.setInterval注意内存占用
由于setInterval一直处于活动状态,造成它所依赖的数据一直无法回收。特别容易出现数据越积越多情况

3.注意闭包
闭包里依赖了主函数的数据,为了让闭包续继访问到数据,必须避免当主函数退出时,回收闭包依赖主函数的变量所对应的数据,从而带来内存溢出风险。

资料:

  1. JS内存管理
  2. How JavaScript works: an overview of the engine, the runtime, and the call stack
  3. JavaScript stack size

留言

欢迎交流想法。留言会通过 GitHub Issues 保存,首次使用需要登录 GitHub。

人生中唯一的2018

  2018年是十分重要的一年,忙碌的一年,有收获的一年。

重要事件

  今年的3.15离开奋战了将近四年的中国软件,入职一家互联网公司,开启职业生涯第三段征程。

收获的

  • 读书:《潮骚》《金阁寺》《万历十五年》《雪国》《小王子》,技术方面很可惜只读了一本《深入浅出Node》,《现代操作系统》读了前面几个章节未继续完整读完
  • 从传统行业进入互联网行业,适应了互联网行业的工作与做事方式、思考方式;从连续三个季度的绩效A,看起来转型还算成功
  • 对前端技术栈有了全面的了解与实践;Vue、React、Node、小程序、Webpack、持续集成、日志监控

留下的

  今年都做了些什么?头脑里第一个冒出来的就是今年工作中做的事儿,今年是转行的第一年,不敢有丝毫的怠慢,大部分精力都投入到了工作中。

  • 参与了团队大部分方向十余个项目的开发
  • 产出了移动端组件库V1.0版本,提供17个基础与业务组件;完整的在线文档与样例、构建与发布流程
  • 推动持续集成在团队中的全面落地
  • 产出了React开发模板、Node开发模板、统一的模板初始化CLI工具
  • 推动日志监控的落地;Node服务错误日志监控与上报、前端错误日志监控与上报、小程序日志监控

其它

  • 今年博客产出锐减,希望在2019多思考多总结
  • 英语学习中断了好长一段时间了,该坚持

2019

  • 焦虑,希望在技术方面能有更多的提升。
  • 多阅读、多思考,看清前路的方向。
  • 时刻做好被淘汰的准备。

留言

欢迎交流想法。留言会通过 GitHub Issues 保存,首次使用需要登录 GitHub。

每一条评论都是我继续分享下去的源动力

  近三年的时间里,断断续续的写了100多篇文章,很少有收到对文章的评论。因此,时不时总会感受到一丝丝的失落。这时,只能把它当作个人的总结归纳与成长。

  看到上面这条评论,很欣慰……

  平时自己也总读其他作者的文章,悄悄的去,然后悄悄的走。从今天开始,我觉得今后不论文章怎么样,多少都应该给原创者表示表示,留下点什么。一个赞、一个顶、一个评论,以示对作者无私分享的感激,鼓励他继续创作下去。

  在CSDN上,之前觉得应当尽量只发一些技术类的文章,个人所思所想放在自己独立博客的一亩三分地上。甚至在最近,发现CSDN不断没落下去,广告奇多,准备放弃CSDN了。但是,看到这条评论后,我觉得还是应该留下来,同时,把有价值的所思所想分享出来,以让更多人看到为目的。对于技术分享,影响的范围可能只是一个bug的解决,而思想感悟的分享,影响面则会更大。而且,封闭的思想,注定也会是孤独的……

  回想起决定写博客的初心,是想“把自己知道的东西分享出来,总会帮助到一些人”。现在看来,总会帮助到一些人这一宏大愿景,值得更坚定不移的执行下去。

  每一条评论都是我继续分享下去的源动力,Thanks!

留言

欢迎交流想法。留言会通过 GitHub Issues 保存,首次使用需要登录 GitHub。

前端getTime时区问题

问题

  发现客户端选择下单时间后,提交订单并查看订单,在订单上显示的之前用户之前选择的时间存在1-N小时不等的差值。
  例如:在下单页选择的2018-01-01 10:00,下单完成后,在订单详情页上面显示为2018-01-01 09:00,与实际的相差1小时。

分析

  结合代码分析,发现当用户选择完时间后,会把选择的时间字符串调用getTime方法转换为毫秒数传给后端,后端再转换回时间字符串,在这个转换过程中,时间出现了差值。
  最终,排查出问题原因为时区问题。还是以上面的样例为范本。当客户端的时区为东京,服务器端的时区为北京就能完美复该问题。
  使用getTime方法获取时间的毫秒数时,实际上的获取当前的格林威治时间1970年1月1日之间的毫秒数。东京时间为东9区,比标准时间快9个小时,所以在东京时区下时间转毫秒数就会减去9个小时才会为标准时间。此时北京时间与标准时间为8个小时的时差,客户端用东京时间转换为毫秒数(减9个小时),服务器端再用北京时间把毫秒数转换为时间字符串(加8个小时),此时就出现了时间差。

解决

  1. 避免使用getTime,前后端直接使用时间字符串传输
  2. 前端对时区就行修复,代码如下
1
2
3
4
5
6
7
8
9
10
11
12
function getTruthTime (time) {
// 服务器端时区,北京东八区(-8*60)
let timezone = -480
// 客户端实际时区(例如东京为东九区:-540)
let offsetGMT = new Date().getTimezoneOffset()
// 计算差值(用户在东京时区下下单,需要补回1小时)
let adjust = timezone - offsetGMT
// 需要修复的时间,支持字符串传参
let nowDate = time ? new Date(time).getTime() : new Date().getTime()
let timeStamp = new Date(nowDate + adjust * 60 * 1000)
return timeStamp.getTime()
}

留言

欢迎交流想法。留言会通过 GitHub Issues 保存,首次使用需要登录 GitHub。

HTTP协议与前后端联调

介绍

  在前后端分离的开发场景下,不可避免的会有前后端联调。在联调阶段,经常会遇到各式各样的问题,比如乱码问题、前端传的数据(字符串、数组、Json对象)后端无法正常解析等问题。
  本文希望从源头着手,理清问题的根本原因,快速定位出现问题的位置,让前后端联调得心应手,让甩锅不再那么容易……

HTTP协议

  之所以这里会介绍一下HTTP协议,是因为前后端联调离不开HTTP。了解了HTTP协议,有助于更好的理解数据传输的流程,以及更好的分析出到底是在哪个环节出了问题,方便排查。

1. 简介

  首先,http是一个无状态的协议,即每次客户端和服务端交互都是无状态的,通常使用cookie来保持状态。
  下图为http请求与响应的大致结构(本部分配图均来自于《HTTP权威指南》):

说明:
  从上图中可以看出,HTTP请求大致分为三个部分:起始行、首部、主体。在请求起始行里,表面了请求方法、请求地址以及http协议的版本。另外,首部即是我们常说的http header。

2. HTTP method

  下面是常用的HTTP请求方法以及介绍:

说明:

  1. 我们常用的一般为get于post。
  2. 是否包含主体的意思为请求内容是否带主体。例如,在get方式下由于不带主体,只能使用url的方式传参。

3. Content-type

  HTTP传输的内容类型与编码是由Content-Type来控制的,客户端与服务端通过它来识别与解析传输内容。

常见的Content-Type:

类型 说明
text/html html类型
text/css css文件
text/javascript js文件
text/plain 文本文件
application/json json类型
application/xml xml类型
application/x-www-form-urlencoded 表单,表单提交时的默认类型
multipart/form-data 附件类型,一般为表单文件上传

  前面六个为常见的文件类型,后面两个为表单数据提交时类型。我们ajax提交数据时一般为Content-Type:application/x-www-form-urlencoded;charset=utf-8,以此声明了本次请求的数据格式与数据编码方式。需要额外说明的是,application/x-www-form-urlencoded此种类型比较特殊,数据发送时会把表单数据拼接成类似于a=1&b=2&c=3的格式,如果数据中存在空格或特殊字符,会进行转换,标准文档在
这里,更详细的在 [RFC1738]可见。

相关资料:

  1. Content-type对照表:http://tool.oschina.net/commons
  2. Form content types:https://www.w3.org/TR/html4/interact/forms.html#h-17.13.4
  3. 字符解码时加号解码为空格问题探究:http://muchstudy.com/2017/12/06/字符解码时加号解码为空格问题探究/
  4. 理解HTTP之Content-Type:http://homeway.me/2015/07/19/understand-http-about-content-type/

4. 字符集与编码

  前后端联调之所以需要了解这部分,是因为在前后端的数据交互中,经常会碰到乱码的问题,了解了这块内容,对于解决乱码问题就手到擒来了。

一图胜千言:

  在图中,charset的值为iso-8859-6,详细介绍了一个文字从编码到解码,再到显示的完整过程。

相关资料:

  1. 字符集列表:https://www.iana.org/assignments/character-sets/character-sets.xhtml
  2. 字符编码详解:http://muchstudy.com/2016/08/26/字符编码详解/

前端部分

  前端部分负责发起HTTP请求,前端常用的HTTP请求工具类有jqueryaxiosfetch。实际上jquery与axios的底层都是使用XMLHttpRequest来发起http请求的,fetch属于浏览器内置的发起http请求方法。

前端ajax请求样例:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qs/6.5.1/qs.min.js"></script>
<title>前端发起HTTP请求样例</title>
</head>
<body>
<h2>使用XMLHttpRequest</h2>
<button onclick="xhrGet()">XHR Get</button>
<button onclick="xhrPost()">XHR Post</button>
<h2>使用axios</h2>
<button onclick="axiosGet()">Axios Get</button>
<button onclick="axiosPost()">Axios Post</button>
<h2>使用fetch</h2>
<button onclick="fetchGet()">Fetch Get</button>
<button onclick="fetchPost()">Fetch Post</button>
<script>
// 封装XMLHttpRequest发起ajax请求
let Axios = function({url, method, data}) {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.onreadystatechange = function() {
// readyState == 4说明请求已完成
if (xhr.readyState == 4 && xhr.status == 200) {
// 从服务器获得数据
resolve(xhr.responseText)
}
};
if(data){
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded;charset=utf-8');
xhr.send(Qs.stringify(data));
//xhr.send("a=1&b=2");
//xhr.send(JSON.stringify(data));
}else{
xhr.send();
}
})
}
// 需要post提交的数据
let postData = {
firstName: 'Fred',
lastName: 'Flintstone',
fullName: '姓 名',
arr:[1,2,3]
}
// 请求地址
let url = 'DemoServlet';

function xhrGet(){
Axios({
url: url+'?a=1',
method: 'GET'
}).then(function (response) {
console.log(response);
})
}
function xhrPost(){
Axios({
url: url,
method: 'POST',
data: postData
}).then(function (response) {
console.log(response);
})
}

function axiosGet(){
// 默认Content-Type = null
axios.get(url, {
params: {
ID: '12345'
}
}).then(function (response) {
console.log(response);
}).catch(function (error) {
console.log(error);
});
}
function axiosPost(){
// 默认Content-Type = application/json;charset=UTF-8
axios.post(url, postData).then(function (response) {
console.log(response);
}).catch(function (error) {
console.log(error);
});
// 默认Content-Type = application/x-www-form-urlencoded
axios({
method: 'post',
url: url,
data: Qs.stringify(postData)
}).then(function (response) {
console.log(response);
});
}

function fetchGet(){
fetch(url+'?id=1').then(res => res.text()).then(data => {
console.log(data)
})
}
function fetchPost(){
fetch(url, {
method: 'post',
body: postData
})
.then(res => res.text())
.then(function (data) {
console.log(data);
})
.catch(function (error) {
console.log('Request failed', error);
});
}
</script>
</body>
</html>

相关资料:

  1. XMLHttpRequest Standard:https://xhr.spec.whatwg.org/
  2. Fetch Standard:https://fetch.spec.whatwg.org/
  3. XMLHttpRequest介绍:https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest
  4. fetch介绍:https://developers.google.com/web/updates/2015/03/introduction-to-fetch
  5. fetch 简介: 新一代 Ajax API: https://juejin.im/entry/574512b7c26a38006c43567c
  6. axios源码:https://github.com/axios/axios

后端部分

  这里使用Java平台为样例来介绍后端是如何接收HTTP请求的。在J2EE体系下,数据的接收与返回实际上都是通过Servlet来完成的。

Servlet接收与返回数据样例:

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
package com.demo.servlet;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.Enumeration;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class DemoServlet extends HttpServlet {
private static final long serialVersionUID = 1L;

public DemoServlet() {
super();
}

protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("-----------------start----------------");
System.out.println("Content-Type:" + request.getContentType());
// 打印请求参数
System.out.println("=========请求参数========");
Enumeration<String> em = request.getParameterNames();
while (em.hasMoreElements()) {
String name = (String) em.nextElement();
String value = request.getParameter(name);
System.out.println(name + " = " + value);
response.getWriter().append(name + " = " + value);
}
// 从inputStream中获取
System.out.println("===========inputStream===========");
StringBuffer sb = new StringBuffer();
String line = null;
try {
BufferedReader reader = request.getReader();
while ((line = reader.readLine()) != null)
sb.append(line);
} catch (Exception e) {
/* report an error */
}
System.out.println(sb.toString());
System.out.println("-----------------end----------------");
response.getWriter().append(sb.toString());
}

protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}

相关资料:

  1. servlet的本质是什么,它是如何工作的?:https://www.zhihu.com/question/21416727
  2. tomcat是如何处理http请求的?:https://blog.csdn.net/qq_38182963/article/details/78660777

结果汇总

| 请求方式 | method | 请求Content-Type | 数据格式 | 后端收到的Content-Type | 能否通过getParameter获取数据 | 能否通过inputStream获取数据 | 后端接收类型 |
|–|
| XHR | Get | 未设置 | url传参 | null | 能 | 否 | 键值对 |
| XHR | Post | 未设置 | json字符串 | text/plain;charset=UTF-8 | 否 | 能 | 字符串 |
| XHR | Post | 未设置 | a=1&b=2格式字符串 | text/plain;charset=UTF-8 | 否 | 能 | 字符串 |
| XHR | Post | application/x-www-form-urlencoded | a=1&b=2格式字符串 | application/x-www-form-urlencoded | 能 | 否 | 后端收到key为a和b,值为1和2的键值对 |
| XHR | Post | application/x-www-form-urlencoded | json字符串 | application/x-www-form-urlencoded | 能 | 否 | 后端收到一个key为json数据,值为空的键值对 |
| axios | Get | 未设置 | url传参 | null | 能 | 否 | 键值对 |
| axios | Post | 未设置 | json对象 | application/json;charset=UTF-8 | 否 | 能 | json字符串 |
| axios | Post | 未设置 | 数组 | application/json;charset=UTF-8 | 否 | 能 | 数组字符串 |
| axios | Post | 未设置 | a=1&b=2格式字符串 | application/x-www-form-urlencoded | 能 | 否 | 键值对 |
| fetch | Get | 未设置 | url传参 | null | 能 | 否 | 键值对 |
| fetch | Post | 未设置 | a=1&b=2格式字符串 | text/plain;charset=UTF-8 | 否 | 能 | a=1&b=2字符串 |
| fetch | Post | 未设置 | json对象 | text/plain;charset=UTF-8 | 否 | 能 | 后端收到[object Object]字符串 |
| fetch | Post | application/x-www-form-urlencoded;charset=UTF-8 | a=1&b=2格式字符串 | application/x-www-form-urlencoded;charset=UTF-8 | 能 | 否 | 键值对 |

  通过上面的表格内容可以发现,凡是使用get或者content-type为application/x-www-form-urlencoded发送数据,在后端servlet都会默认把数据转换为键值对。否则,需要从输入流中获取前端发送过来的字符串数据,再使用fastJSON等后端工具类转换为Java实体类或集合对象。

联调工具Postman

  可以在chrome的应用商店中下载Postman插件,在浏览器中模拟HTTP请求。Postman的界面如下:

说明:

  1. 发送get请求直接在url上带上参数,接着点击send即可
  2. 发送post请求,数据有三种传输方式;form-datax-www-form-urlencodedraw(未经加工的)
类型 Content-Type 说明
form-data Content-Type: multipart/form-data form表单的附件提交方式
x-www-form-urlencoded Content-Type: application/x-www-form-urlencoded form表单的post提交方式
raw Content-Type: text/plain;charset=UTF-8 文本的提交方式

留言

欢迎交流想法。留言会通过 GitHub Issues 保存,首次使用需要登录 GitHub。

Gitlab 8.x runner安装与配置

介绍

  Gitlab 8.x之后默认集成了Gitlab CI,意味着支持了持续集成相关功能。每一次集成操作都需要对应的runner来跑代码构建、测试、发布等操作。Runner实际上就是为Gitlab的持续集成指定一个环境。

安装

官方文档地址:https://docs.gitlab.com/runner/install/

  Gitlab Runner的版本需要跟Gitlab对应,这里有一个对照表。最新的版本对照表中并没有Gitlab8.X对应的Runner版本,查了一下Gitlab8.X对应的Runner版本为1.X,所以这里选择runner 1.11.2版本。

  这里运行Gitlab与Runner的环境均为CentOS,之前尝试在windows上安装runner,对接Linux上的Gitlab,发现在Gitlab runner运行的控制台出现乱码问题。

0.准备

在opt下创建gitlab-runner目录并进入该目录,后续执行的操作与所有的资源都放在这个目录中

1
2
3
cd /opt
mkdir gitlab-runner
cd gitlab-runner/

1.下载

下载安装资源到gitlab-runner目录中

1
sudo wget https://gitlab-ci-multi-runner-downloads.s3.amazonaws.com/v1.11.2/binaries/gitlab-ci-multi-runner-linux-386

2.添加运行权限

1
sudo chmod +x gitlab-ci-multi-runner-linux-386

3.创建用户

1
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash

4.安装

1
2
./gitlab-ci-multi-runner-linux-386 install --user=gitlab-runner --working-directory=/opt/gitlab-runner
sudo gitlab-ci-multi-runner-linux-386 start

配置

  经过上面的步骤,Runner就已经跑起来了,剩下的还需要Runner与项目对接起来。Runner的类型分为Shared, specific and group Runners。这里选择specific类型,即单独的项目使用。

  在Gitlab项目的setting-runner中,配置过程中会使用到urltoken如下所示:

1.运行register命令

1
./gitlab-ci-multi-runner-linux-386 register

之后就按照提示就行了

2.输入url地址
3.输入token
4.输入描述,任意即可
5.输入标签,这里直接Enter跳过
6.选择Runner executor,这里选择shell

到这里就已经注册成功了,输入./gitlab-ci-multi-runner-linux-386 list就能看到上面的注册的条目。

官方文档地址:https://docs.gitlab.com/runner/register/index.html

其它

  上面两个步骤做完后,此时按理说Gitlab就能调用Runner跑持续集成了,实际当中还会碰到其它问题,整理如下。

权限问题

  如果在Gitlab的Build控制台上报无法创建文件夹无法运行bash等,证明创建的GitLab Runner权限不够。
此时,我这里是修改GitLab Runner的权限跟root保持一致。

1
vim /etc/passwd

通过上面命令可以编辑用户对应的权限,我这里打开默认为gitlab-runner:x:601:601:GitLab Runner:/home/gitlab-runner:/bin/bash,权限组修改为跟root的一致gitlab-runner:x:0:0:GitLab Runner:/home/gitlab-runner:/bin/bash。(root的权限组名为0)

这里在另外一台机器上还碰到这样修改了也不好使的问题,最终gitlab-runner install的时候,直接指定为root,而不新创建用户。

环境问题

由于Runner运行需要环境支撑,比如git、node、npm等,需要在Runner所在的服务器上准备好所有的依赖。

  • Linux Node安装
1
2
3
4
5
6
7
# 下载
wget https://nodejs.org/dist/v8.11.3/node-v8.11.3-linux-x64.tar.xz
# 解压
tar -xf node-v8.11.3-linux-x64.tar.xz
# 建立软链接,实现全局访问
ln -s /opt/gitlab-runner/node-v8.11.3-linux-x64/bin/node /usr/local/bin/node
ln -s /opt/gitlab-runner/node-v8.11.3-linux-x64/bin/npm /usr/local/bin/npm

此时,输入node -v就能看到node的版本了。

使用软连接方式可能对非root用户无效,可以转而使用配置环境变量的方式

1
2
3
4
5
6
7
8
# 修改配置文件
vim /etc/profile
#set for nodejs,新增NODE_HOME并放到PATH上
export JAVA_HOME=/opt/soft/java
export NODE_HOME=/opt/gitlab-runner/node-v8.11.3-linux-x64
export CLASSPATH=$JAVA_HOME/lib:$JAVA_HOME/jre/lib
export PATH=$PATH:$JAVA_HOME/bin:$NODE_HOME/bin

在vim环境下点击i进入插入状态,编辑完成后按Esc键,然后输入 :wq 按回车保存退出。

备注:内外环境还需修改NPM的镜像源,比如修改为npm config set registry https://registry-npm.daojia-inc.com/

附录 部分GitLab-Runner常用命令

1.gitlab-runner帮助:gitlab-runner –help

2.gitlab-runner指定命令帮助:gitlab-runner –help

3.注册runner:gitlab-runner register

4.注销runner:gitlab-runner unregister

5.当前运行的runner:gitlab-runner list

6.启动runner:gitlab-runner start

7.停止runner:gitlab-runner stop

8.重启runner:gitlab-runner restart

9.查询runner状态:gitlab-runner status

留言

欢迎交流想法。留言会通过 GitHub Issues 保存,首次使用需要登录 GitHub。

一个由line-height引发的血案与思考

爆炸

  最近UI走查,发现页面中所有包含文字区块的高度与设计稿中的高度完全不一致,然后UI妹子就爆炸了!

  找了一下原因,发现是由于UI设计稿中设计的文字大部分是font-size:24px;line-height:24px,代码实现时为了不至于每处都写一遍字体大小,故直接在根节点上统一设置字体与字体大小为24px,小部分不一致的地方再单独设置字体大小,从而忽略了设置line-height为字体的高度。造成的结果就是文字所在的行的行高高于设计稿中的行高。

为什么文字行高与字体大小不相等呢?

  翻了一下line-height的官方说明,如下所示:

  文档里说line-height的默认值为normal,给normal的推荐设置值为1.0到1.2之间。相当于如果设置了字体的大小而不设置line-height,那么行高默认就为字体的1.0-1.2之间的一个倍数。

  那么,这个倍数到底具体是多少呢?

  在chrome的控制台里跟踪了一下,看到项目中引入了normalize.css来初始化浏览器的默认css样式。其中,就设置html的line-height1.15

为什么normalize.css设置line-height默认为1.15?

  翻了一下github中normalize.css的issues,在593号issues里找到了答案,地址在这里

  大致过程是这样的,有人发现相同字体与大小的文字在不同环境中line-height的值是不一致的,接着,就有人在crossbrowsertesting上做了个测试,得出的结论就是这个问题的的确确存在,而且差异还特别大

When the font size was 100px, the most common line height was 115px. However, results varied from 101px on Mac Firefox to 136px on Android Chrome.

  最终,由于大部分的行高都为115px,所以,为了解决不同环境中相同字体与字号的文字行高不一致的问题,推荐设置默认line-height为1.15

  看到这里,想到一个问题,既然显示设置line-height为1.15是为了解决环境兼容的问题。那么,为什么不设置line-height:1即解决兼容问题,又解决由于行高放大与UI设计稿不符的问题?

设置overflow:hidden字体显示不全的问题

  当设置文字line-height:1后,再设置文字所在的容器overflow:hidden很容易复现文字显示不全的问题,比如下面:

dphyTxg

代码如下:

1
2
3
4
5
6
<span style="
font-size:100px;
line-height:1;
overflow:hidden;
border:1px solid #ddd;
display:inline-block">dphyTxg</span>

注:这里可以使用div演示,然后去掉display:inline-block。使用div发现不知道为毛markdown里的样例乱了…

为什么设置行高与字体大小一致文字会显示不全?

  到这里,才真正进入到深层次的原因探究。

  对于这个问题,需要先了解字体度量,引用一下其它的文章说明,如下所示:

  1. 原文地址
  2. Deep dive CSS: font metrics, line-height and vertical-align
  3. EM Square
  4. FontForge

简要解释为如下几点:

  1. 我们设置font-size的高度实际上是对应字体的EM square部分
  2. 字体在设计时可以超出EM square部分
  3. 字体实际设计与EM square部分一致时,line-height:normalline-height:1相等
  4. 从上图中可以看到,ascender+descender>Em Size1100+540>1000,此时line-height:1.64,所以100px的文字默认的行高为164px

样例分析

  下面就以window环境下的默认字体微软雅黑为例实际看看line-height的计算。

h1

line-height:

代码如下:

1
2
3
4
5
<h1 id="font" style="line-height:normal;font-size: 100px;font-family: 微软雅黑">h1</h1>
<p>line-height: <code id="o1" style="font-size: 50px"></code></p>
<script>
document.getElementById('o1').textContent = window.getComputedStyle(document.getElementById('font')).height;
</script>

  从上面可以看到微软雅黑100px情况在,在windows下行高显示为132px。通过FontForge来核对一下看看是不是这样的。

{% asset_img 3.jpg %}

  从上图中可以看到,使用(ascender+descender)/Em Size,即(2167+536)/2048≈1.32,也就是line-height为1.32.

结论

  1. line-height:normal的值跟字体有关系,相同字体在不同环境也不一样
  2. 当line-height设置的值小于默认的值时就会存在显示不全的问题
  3. normalize.css设置line-height默认为1.15,当字体line-height的normal大于1.15就会存在文字显示不全的问题
  4. 要解决字体在不同平台line-height不一致的问题,需根据具体字体,选择normal在不同平台上的最大值设置

留言

欢迎交流想法。留言会通过 GitHub Issues 保存,首次使用需要登录 GitHub。

30

  30岁是一道坎。

  从年龄上来讲,又是一个十年。人生就这样过完了三分之一,不由得心生感慨!感慨时光如梭,感慨事业无成,感概没有了上一个十年时的锐气,就连眼神也没有了那时的干净清澈。从十年前的白面书生变成了中年油腻男,时间真的是一把杀猪刀!

  从工作与生活上来讲,三十而立。但很多人在这个时候面对着上有老下有有小往往却力不从心。需要咬紧牙关坚持下去,翻过这道坎。

  就在前几天,我也进入了而立之年。生日当天,那一整天仿佛都充满了仪式感,仿佛过完了那一天就进入到了一个新的人生阶段。当时灵感爆棚(也可能是一时冲动,人很多时候会把冲动当作灵感),希望能记下点什么,但什么都没记下……

  今年的315入职新公司,在三十岁之前从传统行业转到互联网行业。之所以转,是因为觉得在传统行业中已经到了天花板,进入了舒适区,不想温水煮青蛙,想换一种环境看看。从目前的情况来看,似乎还不错,在新的环境里有很多事情值得做,值得拼一下。似乎又回到了刚入职场的状态,这是一个之前一直期望的状态,很满意。

  30岁不仅仅是一道坎,跨过这道坎实际上是一趟新的征程。希望在这趟新的旅程中能一直坚挺下去!

留言

欢迎交流想法。留言会通过 GitHub Issues 保存,首次使用需要登录 GitHub。