开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

为什么多线程下不建议使用SimpleDateFormat 前

发表于 2021-11-16

这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

前言

经常听到开发大佬说不建议用SimpleDateFormat,包括阿里巴巴java开发手册也写了“SimpleDateFormat是线程不安全的类,一般不要定义为static变量,如果定义为static,必须加锁,或者使用 DateUtils 工具类。”

image.png
可是,为什么说SimpleDateFormat是线程不安全的类呢?要想证明这个问题,首先要聚一个例子,看看在多线程环境下,使用SimpleDateFormat会怎么样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typescript复制代码public static void main(String[] args) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
CountDownLatch countDownLatch = new CountDownLatch(30);
for (int i = 0; i < 30; i++) {
CompletableFuture.runAsync(() -> {
//先获取当前时间的格式化字符串
String str1 = simpleDateFormat.format(new Date());
try {
//在通过格式化字符串再操作一次
Date parseDate = simpleDateFormat.parse(str1);
String str2 = simpleDateFormat.format(parseDate);
//对比前后格式化字符串是否一致
System.out.println(String.format("threadId = %s , 前 = %s , 后 = %s", Thread.currentThread().getId(), str1, str2));
} catch (Exception ignored) {
} finally {
countDownLatch.countDown();
}
}, ThreadPoolUtil.pool);
}
countDownLatch.await();
}

在同个一个线程中,多次操作同一个时间new Date(),比较前后操作的结果是否一样。一样,则代表是现场安全的。

在看一下运行的结果

image.png

发现确实在某些线程中,前后的时间不一致。那就说明SimpleDateFormat确实是线程不安全的。

看一下format方法的源码

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
ini复制代码private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// Convert input date to time field list
calendar.setTime(date);

boolean useDateFormatSymbols = useDateFormatSymbols();

for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}

switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
toAppendTo.append((char)count);
break;

case TAG_QUOTE_CHARS:
toAppendTo.append(compiledPattern, i, count);
i += count;
break;

default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}

可以看到,多个线程之间共享变量calendar,并修改calendar。因此在多线程环境下,当多个线程同时使用相同的SimpleDateFormat对象(如static修饰)的话,如调用format方法时,多个线程会同时调用calender.setTime方法,导致time被别的线程修改,因此线程是不安全的。

此外,parse方法也是线程不安全的,parse方法实际调用的是CalenderBuilder的establish来进行解析,其方法中主要步骤不是原子操作。
想要避免,要不就加锁、要不就在方法内new SimpleDateFormat。不管是哪种,都是对性能有所影响的。JDK8的DateTimeFormatter是一个不错的解决方式。我们来看看DateTimeFormatter是怎么保证线程安全的。

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
swift复制代码public final class DateTimeFormatter {

/**
* The printer and/or parser to use, not null.
*/
private final CompositePrinterParser printerParser;
/**
* The locale to use for formatting, not null.
*/
private final Locale locale;
/**
* The symbols to use for formatting, not null.
*/
private final DecimalStyle decimalStyle;
/**
* The resolver style to use, not null.
*/
private final ResolverStyle resolverStyle;
/**
* The fields to use in resolving, null for all fields.
*/
private final Set<TemporalField> resolverFields;
/**
* The chronology to use for formatting, null for no override.
*/
private final Chronology chrono;
/**
* The zone to use for formatting, null for no override.
*/
private final ZoneId zone;

DateTimeFormatter的类是用final修饰的,成员变量也是用final修饰的。这就代表DateTimeFormatter是个不可变的对象,意味着线程B对象无法修改线程A的DateTimeFormatter对象中的变量。线程B只能自己新建一个DateTimeFormatter对象。

总结

简述了SimpleDateFormat为什么是线程不安全的和DateTimeFormatter为什么是线程安全的之后,我们在实际开发中,要努力去规范我们的代码,在使用封装好的工具类时,也要知其原理,避免问题。

如果项目是JDK8的话,在处理时间问题时,不妨直接用DateTimeFormatter来试试!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

五分钟,请将服务器A的数据库迁移到B中 实战篇

发表于 2021-11-16

这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

前言

  • 在线音乐戳我呀!
  • 音乐博客源码上线啦!
  • 前几篇讲了Docker的安装部署,操作算说比较简单的。
  • 接下来将分享如何将服务器A的数据库迁移(导入)到服务器B的数据库中,一五一十盘出。
  • Are you ready ?

上个星期去了韶关云门寺拜香。烟雾缭绕仿佛想起在仙界的日子。怀念呀~

1.jpg

将服务器A的oracle数据库导入到服务器B的数据库中

  • 如何创建表空间?
  • 如何创建用户?
  • 开始导入。

导入数据库的时候,不是说创建用户、数据库,就想直接导入。
oracle和mysql的不同之处就是:oracle有表空间。
所以别忘记先创建表空间。

环境

  • 数据库:Oracle
  • 服务器:Oracle数据库是在Docker中运行

一、如何创建表空间?

1.1 创建文件夹给表空间文件存放

在/home/oracle下创建tablespace文件夹。

mkdir /home/oracle/tablespace

1.2 创建表空间

表空间分两种,有临时和常规两种类型。

  • 创建临时表空间TEST_TEMP
1
2
3
4
5
6
7
8
9
10
11
sql复制代码CREATE TEMPORARY TABLESPACE TEST_TEMP

         TEMPFILE '/home/oracle/tablespace/TEST_TEMP.DBF'

         SIZE 32M

         AUTOEXTEND ON

         NEXT 32M MAXSIZE UNLIMITED

         EXTENT MANAGEMENT LOCAL;
  • 创建表空间TEST
1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码CREATE TABLESPACE TEST

         LOGGING

         DATAFILE '/home/oracle/tablespace/TEST.DBF'

         SIZE 32M

         AUTOEXTEND ON

         NEXT 32M MAXSIZE UNLIMITED

         EXTENT MANAGEMENT LOCAL;

二、如何创建用户?

创建用户test,密码是123456

1
2
3
4
5
6
7
sql复制代码CREATE USER test IDENTIFIED BY 123456

         ACCOUNT UNLOCK

         DEFAULT TABLESPACE TEST

         TEMPORARY TABLESPACE TEST_TEMP;

赋予用户权限。

1
2
3
sql复制代码GRANT CONNECT,RESOURCE TO ithinkdt;

GRANT DBA TO ithinkdt;(可选,DBA为数据库管理员权限)

那现在我不小心创建用户了,但我想重新给这个用户指定表空间,那应该怎么做?

1
2
3
4
sql复制代码alter user username default tablespace userspace;

# 例子:给u1指定syg表空间。
alter user u1 default tablespace syg;

三、开始导入

前期工作(表空间、用户)已准备好,进行导入工作。

3.1 理清步骤

需求是:将服务器A的数据库导入到服务器B的数据库中。

那肯定有A数据库的dmp文件吧,先把dmp文件上传到服务器B中,然后再拷贝到Docker的Oracle容器中。

3.2 如何将dmp文件拷贝到oracle容器中?

命令是:docker cp /服务器文件路径 容器id:/存放路径

3.3 导入

Oracle容器中已经有dmp文件了,这时我们进入Oracle容器中,切换成root用户,执行导入数据库的命令。

忘记进入Oracle容器的朋友,请戳:实战篇:Oracle不会部署?来,教你玩

导入dmp文件的命令:

1
sql复制代码imp test/123456@39.542.126.84:1521/hellowinxDB file=/home/data.dmp full=y;

参数说明

  • imp:
+ 导入的命令,它的兄弟导出是exp。
  • test/123456:
+ 用户名/密码。
  • 39.542.126.84:1521/hellowinxDB:
+ 服务器ip:端口号/实例名。
+ 注意前面是用@符号来连接的。
  • file=/home/data.dmp:
+ dmp文件的路径。
  • full=y:
+ 导入文件中全部内容,有可能有多个用户的内容。
+ 导出除ORDSYS,MDSYS,CTXSYS,ORDPLUGINS,LBACSYS 这些系统用户之外的所有用户的数据。
+ 如果你联接的用户默认使用的表空间设置成system,则导入文件的内容会导到system上。

2.png

3.4 可能会导入失败。

如果运气好,那就没这一步了,但如果导入失败的朋友,请继续往下看。

导入失败的其中一个原因:版本问题。

查看数据库版本是多少:

1
sql复制代码select * from product_component_version;

3.png

很明显,两个数据库版本确实是不一样,那怎么解决?

有两个方案:

  • 如果是新建的Oracle数据库,那就删除掉,跟服务器A的数据库保持一致的版本。
  • 那如果不想删除数据库,直接改dmp文件的oracle版本ETX ETX TEXPORT

修改oracle版本:

4.png

3.5 可能会乱码。

编码不一样,就会导致乱码。这是很常见的事情。

其根本原因主要是数据编码不一致导致。根据其原理排查数据编码,基本都可以解决乱码问题。

5.png

解决方法:查看编码。

1
2
3
4
5
sql复制代码# 查看oracle数据库字符集:
select userenv('language') from dual;

# 查看oracle数据库的编码
select * from nls_database_parameters where parameter ='NLS_CHARACTERSET';

后面我改了服务端的字符集为gbk就显示正常了。

1
sql复制代码alter database character set internal_use ZHS16GBK;

延伸情况

3.5.1 oracle和java

如果plsql查出来不会乱码,但通过java打断点查出来乱码,那可能是oracle数据库的编码不是utf-8,因为java默认是utf-8

3.5.2 oracle和系统环境变量

如果还是乱码的话,在看看本地的环境变量。

添加系统环境变量NLS_LANG,将其值与数据库编码设置保持一致,即上一步查询的查询结果SIMPLIFIED CHINESE_CHINA.ZHS16GBK。点击保存即可。

6.png

关闭当前的客户端查询工具,重新打开查看和验证是否已经恢复正常的中文编码。

后记

接触Oracle的时候,还是在上家前后端都做的日子。其实对于新人都接触对个人生涯我觉得是很有帮助的。

因为前后端都懂的话,有很多好处,就比如:有一个需求来了,可能这个前端做是可以实现的,但后端做,可能会更好(可能对于性能等等来说)

更能知道哪端做会更合适、更优 ~

如果对您有帮助,你的点赞是我前进的润滑剂。

相关文献

DOCKER - ORACLE数据库命令行创建表空间及用户

查看及修改Oracle编码格式方法

Oracle数据库数据显示乱码问题解决方法

以往推荐

尤大大说我的代码全部不加分号

老湿说的万物皆对象,你也信?

Vue-Cli3搭建组件库

Vue实现动态路由(和面试官吹项目亮点)

项目中你不知道的Axios骚操作(手写核心原理、兼容性)

VuePress搭建项目组件文档

koa2+vue+nginx部署

vue-typescript-admin-template后台管理系统

原文链接

juejin.cn/post/703097…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Java入门100例】10各行元素之和——二维数组

发表于 2021-11-16

这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战

点赞再看,养成习惯。微信搜索【一条coding】关注这个在互联网摸爬滚打的程序员。

本文收录于github-技术专家修炼,里面有我的学习路线、系列文章、面试题库、自学资料、电子书等。

题目描述

难度:简单

计算二维数组中各行元素之和并查找其值最大的那个行。

知识点

  • 二维数组
  • 双重循环

解题思路

1.二维数组

二维数组本质上是以数组作为数组元素的数组,即“数组的数组”。可以想像成一个方阵。

定义数组

1
java复制代码int brr [] []=new int[][3]; //二维数组中行可以省略,至少写出列

添加元素

1
2
3
4
5
6
java复制代码brr[0][0]=1; //下标同样从0开始,可以指定位置赋值,也可以整体赋值。
brr={
{1,2,3},
{5,6,7},
{9,10,11}
};

循环遍历

1
2
3
4
5
6
7
java复制代码//遍历行
for(int i=0;i<3;i++){
//遍历列
for(int j=0;j<3;j++){
System.out.println(brr[i][j]);
}
}

2.双重循环

类似上面的循环遍历。

在实际应用中,双重循环能解决大部分问题,但其时间复杂度为O(n^2),尽量避免使用。

代码实现

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
java复制代码/**
*计算二维数组中各行元素之和并查找其值最大的那个行。
*/
public class question_10 {
public static void main(String args[]) {
int myTable[][] = {
{22, 34, 45, 11, 33, 5, 92},
{17, 9, 27, 31, 46, 54, 88},
{98, 81, 64, 62, 15, 14, 23},
{54, 43, 55, 1, 22, 9, 33}
};
int sum, max, maxRow=0;
max = 0;
for (int row=0; row<4; row++) {
sum = 0;
for (int col=0; col<7; col++)
sum += myTable[row][col];
if (sum > max) {
max = sum;
maxRow = row;
}
}
System.out.println("Row: " + maxRow + " value: " + max);
}
}

输出结果

扩展总结

在实际开发中,我们更多使用的集合,关于集合的知识,可以看我这篇:大厂面试突进——集合篇。

最后

独脚难行,孤掌难鸣,一个人的力量终究是有限的,一个人的旅途也注定是孤独的。当你定好计划,怀着满腔热血准备出发的时候,一定要找个伙伴,和唐僧西天取经一样,师徒四人团结一心才能通过九九八十一难。
所以,

如果你想学好Java

想进大厂

想拿高薪

想有一群志同道合的伙伴

请加入技术交流

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

dart系列之 元世界pubspecyaml文件详解 简介

发表于 2021-11-16

「这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战」

简介

pubspec.yaml是所有dart项目的灵魂,它包含了所有dart项目的依赖信息和其他元信息,所以pubspec.yaml就是dart项目的meta!

pubspec.yaml支持的字段

根据dart的定义,pubspec.yaml中可以包含下面的字段:

字段名 是否必须字段 描述
name 是 package的名字
version 如果发布到pub.dev,则需要 package的版本号
description 如果发布到pub.dev,则需要 package的描述信息
homepage 否 package的主页
repository 否 package的源代码地址
issue_tracker 否 package问题跟踪地址
documentation 否 package的文档信息
dependencies 否 package的依赖信息
dev_dependencies 否 pacakge的dev依赖信息
dependency_overrides 否 想要覆盖的package
environment dart2需要
executables 否 package的可执行文件路径
publish_to 否 package将如何发布

注意,以上是dart中pubspec.yaml支持的字段,如果是在flutter环境中,则会有些额外支持的字段。

一个例子

我们看一个具体的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码 name: my_app
version: 11.15
description: >-
this is a new app
homepage: http://www.flydean.com
documentation: http://www.flydean.com
environment:
sdk: '>=2.10.0 <3.0.0'
dependencies:
efts: ^2.0.4
transmogrify: ^0.4.0
dev_dependencies:
test: '>=1.15.0 <2.0.0'

字段详情

下面来看下各个字段的详情和限制情况:

  1. Name

name表示的是包的名字,name必须是全小写,如果有多个词的话,可以用下划线来区分,如:my_app.

并且只能使用小写字母和数字的组合,同时不能以数字开头,并且不要使用dart中的保留字。

  1. Version

Version表示的是版本号,版本号是由点分割的三个数字,如:11.15.0. 后面还可以跟上build版本号:+1, +2, +hotfix.oopsie, 或者预发布版本等:-dev.4, -alpha.12, -beta.7, -rc.5.

  1. Description

package的描述信息最好使用英文来描写,长度是60 到180个字符,表示这个包的作用。

  1. Dependencies

有两种依赖信息,一种是所有使用到这个packages的人都需要用到的依赖,这种依赖放在dependencies中。

还有一种是只用在当前pacakge开发中的包,这种依赖放在dev_dependencies中。

在某些情况下,我们有可能需要覆盖某些依赖包,则可以放在:dependency_overrides中。

  1. Executables

有些pacakges提供的是工具供大家使用,这些工具有可能是命令行工具,所以需要在executables中指定可以执行的命令的路径。

比如下面的配置:

1
2
3
yaml复制代码executables:
slidy: main
fvm:

那么在执行pub global activate之后,就可以在全局执行slidy来执行bin/main.dart, 和fvm来执行binfvm.dart.

  1. environment

因为Dart是一门新的语言,所以目前来说其变动还是挺大的。所以有些应用可以依赖于不同的dart版本,这时候就需要用到environment:

1
2
vbnet复制代码environment:
sdk: '>=2.10.0 <3.0.0'

上面的代码中,我们指定了dart sdk的版本范围。

从dart1.19之后,environment:中还支持指定flutter的版本:

1
2
3
yaml复制代码environment:
sdk: '>=1.19.0 <3.0.0'
flutter: ^0.1.2

总结

以上就是dart的元世界pubspec.yaml详解。

本文已收录于 www.flydean.com/10-dart-pub…

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

动态规划攻略之:杨辉三角

发表于 2021-11-16

「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」

题目

给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。

在「杨辉三角」中,每个数是它左上方和右上方的数的和。

image

示例1:

输入: numRows = 5
输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]

示例2:

输入: numRows = 1
输出: [[1]]

解题思路

根据题意可知,杨辉三角具有以下的特性:

  1. 每行数字左右对称,由 1 开始逐渐变大再变小,并最终回到 1。
  2. 每个数字等于上一行的左右俩个数字之和,即第 n 行的第 i 个数等于第 n-1 行的第 i-1 个数和第 i 个数之和。

由此,我们可以一行一行地计算杨辉三角。每当我们计算出第 i 行的值,我们就可以在线性时间复杂度内计算出第 i+1 行的值。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码class Solution {
public List<List<Integer>> generate(int numRows) {
List<List<Integer>> nums = new ArrayList<List<Integer>>();
for (int i = 0; i < numRows; i++) {
List<Integer> row = new ArrayList<Integer>();
for (int j = 0; j <= i; j++){
if (j == 0 || j == i) {
row.add(1);
} else {
row.add(nums.get(i-1).get(j-1) + nums.get(i-1).get(j));
}
}
nums.add(row);
}
return nums;
}
}

最后

时间复杂度:O(n2),n 为行数;
空间复杂度:O(n2);

往期文章:

  • 二叉树刷题总结:二叉搜索树的属性
  • 二叉树总结:二叉树的属性
  • 二叉树总结:二叉树的修改与构造
  • StoreKit2 有这么香?嗯,我试过了,真香
  • 看完这篇文章,再也不怕面试官问我如何构造二叉树啦!
  • 那帮做游戏的又想让大家氪金,太坏了!
  • 手把手带你撸一个网易云音乐首页 | 适配篇
  • 手把手带你撸一个网易云音乐首页(三)
  • 手把手带你撸一个网易云音乐首页(二)
  • 手把手带你撸一个网易云音乐首页(一)
  • 代码要写注释吗?写你就输了
  • Codable发布这么久我就不学,摸鱼爽歪歪,哎~就是玩儿
  • iOS 优雅的处理网络数据,你真的会吗?不如看看这篇
  • UICollectionView 自定义布局!看这篇就够了

请你喝杯 ☕️ 点赞 + 关注哦~

  1. 阅读完记得给我点个赞哦,有👍 有动力
  2. 关注公众号— HelloWorld杰少,第一时间推送新姿势

最后,创作不易,如果对大家有所帮助,希望大家点赞支持,有什么问题也可以在评论区里讨论😄~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

nginx 核心配置文件结构

发表于 2021-11-16

「这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战」。

往期文章:

初识 Nginx

nginx 的安装

Nginx的核心配置文件默认是放在/usr/local/nginx/conf/nginx.conf

以下为主要部分配置

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
ini复制代码worker_processes  1;

events {
worker_connections 1024;
}

http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;

server {
listen 80;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}

}

指令名 指令值; #全局块,主要设置Nginx服务器整体运行的配置指令

#events块,主要设置,Nginx服务器与用户的网络连接,这一部分对Nginx服务器的性能影响较大
events {
指令名 指令值;
}
#http块,是Nginx服务器配置中的重要部分,代理、缓存、日志记录、第三方模块配置...
http {
指令名 指令值;
server { #server块,是Nginx配置和虚拟主机相关的内容
指令名 指令值;
location / {
#location块,基于Nginx服务器接收请求字符串与location后面的值进行匹配,对特定请求进行处理
指令名 指令值;
}
}
...
}

nginx.conf配置文件中默认有三大块:全局块、events块、http块

http块中可以配置多个server块,每个server块又可以配置多个location块。

全局块

user指令

user:用于配置运行Nginx服务器的worker进程的用户和用户组。

语法 user user [group]
默认值 nobody
位置 全局块

该属性也可以在编译的时候指定,语法如下./configure --user=user --group=group,如果两个地方都进行了设置,最终生效的是配置文件中的配置。

work process指令

master_process:用来指定是否开启工作进程。

语法 master_process on/off;
默认值 master_process on;
位置 全局块

worker_processes:用于配置Nginx生成工作进程的数量,这个是Nginx服务器实现并发处理服务的关键所在。理论上来说workder process的值越大,可以支持的并发处理量也越多,但事实上这个值的设定是需要受到来自服务器自身的限制,建议将该值和服务器CPU的内核数保存一致。

语法 worker_processes num/auto;
默认值 1
位置 全局块

如果将worker_processes设置成2,则会看到如下内容:

111.jpg

其他指令

daemon:设定Nginx是否以守护进程的方式启动。

守护式进程是linux后台执行的一种服务进程,特点是独立于控制终端,不会随着终端关闭而停止。

语法 daemon on/off;
默认值 daemon on;
位置 全局块

pid:用来配置Nginx当前master进程的进程号ID存储的文件路径。

语法 pid file;
默认值 默认为:/usr/local/nginx/logs/nginx.pid
位置 全局块

该属性可以通过./configure --pid-path=PATH来指定

error_log:用来配置Nginx的错误日志存放路径

语法 error_log file [日志级别];
默认值 error_log logs/error.log error;
位置 全局块、http、server、location

该属性可以通过./configure --error-log-path=PATH来指定

其中日志级别的值有:debug|info|notice|warn|error|crit|alert|emerg,翻译过来为试|信息|通知|警告|错误|临界|警报|紧急,这块建议大家设置的时候不要设置成info以下的等级,因为会带来大量的磁盘I/O消耗,影响Nginx的性能。

(5)include:用来引入其他配置文件,使Nginx的配置更加灵活

语法 include file;
默认值 无
位置 any

events块

(1)accept_mutex:用来设置Nginx网络连接序列化

语法 accept_mutex on/off;
默认值 accept_mutex on;
位置 events

这个配置主要可以用来解决常说的”惊群”问题。大致意思是在某一个时刻,客户端发来一个请求连接,Nginx后台是以多进程的工作模式,也就是说有多个worker进程会被同时唤醒,但是最终只会有一个进程可以获取到连接,如果每次唤醒的进程数目太多,就会影响Nginx的整体性能。如果将上述值设置为on(开启状态),将会对多个Nginx进程接收连接进行序列号,一个个来唤醒接收,就防止了多个进程对连接的争抢。

(2)multi_accept:用来设置是否允许同时接收多个网络连接

语法 multi_accept on/off;
默认值 multi_accept off;
位置 events

如果multi_accept被禁止了,nginx一个工作进程只能同时接受一个新的连接。否则,一个工作进程可以同时接受所有的新连接

(3)worker_connections:用来配置单个worker进程最大的连接数

语法 worker_connections number;
默认值 worker_commections 512;
位置 events

这里的连接数不仅仅包括和前端用户建立的连接数,而是包括所有可能的连接数。另外,number值不能大于操作系统支持打开的最大文件句柄数量。

(4)use:用来设置Nginx服务器选择哪种事件驱动来处理网络消息。

语法 use method;
默认值 根据操作系统定
位置 events

注意:此处所选择事件处理模型是Nginx优化部分的一个重要内容,method的可选值有select/poll/epoll/kqueue等,之前在准备centos环境的时候,我们强调过要使用linux内核在2.6以上,就是为了能使用epoll函数来优化Nginx。

另外这些值的选择,我们也可以在编译的时候使用

--with-select_module、--without-select_module、

--with-poll_module、--without-poll_module来设置是否需要将对应的事件驱动模块编译到Nginx的内核。

events指令配置实例

打开Nginx的配置文件 nginx.conf,添加如下配置

1
2
3
4
5
6
ini复制代码events{
accept_mutex on;
multi_accept on;
worker_commections 1024;
use epoll;
}

启动测试

1
2
bash复制代码./nginx -t
./nginx -s reload

http块

定义MIME-Type

我们都知道浏览器中可以显示的内容有HTML、XML、GIF等种类繁多的文件、媒体等资源,浏览器为了区分这些资源,就需要使用MIME Type。所以说MIME Type是网络资源的媒体类型。Nginx作为web服务器,也需要能够识别前端请求的资源类型。

在Nginx的配置文件中,默认有两行配置

1
2
ini复制代码include mime.types;
default_type application/octet-stream;

(1)default_type:用来配置Nginx响应前端请求默认的MIME类型。

语法 default_type mime-type;
默认值 default_type text/plain;
位置 http、server、location

在default_type之前还有一句include mime.types,include之前我们已经介绍过,相当于把mime.types文件中MIMT类型与相关类型文件的文件后缀名的对应关系加入到当前的配置文件中。

举例来说明:

有些时候请求某些接口的时候需要返回指定的文本字符串或者json字符串,如果逻辑非常简单或者干脆是固定的字符串,那么可以使用nginx快速实现,这样就不用编写程序响应请求了,可以减少服务器资源占用并且响应性能非常快。

如何实现:

1
2
3
4
5
6
7
8
9
bash复制代码location /get_text {
#这里也可以设置成text/plain
  default_type text/html;
  return 200 "This is nginx's text";
}
location /get_json{
  default_type application/json;
  return 200 '{"name":"TOM","age":18}';
}

自定义服务日志

Nginx中日志的类型分access.log、error.log。

access.log:用来记录用户所有的访问请求。

error.log:记录nginx本身运行时的错误信息,不会记录用户的访问请求。

Nginx服务器支持对服务日志的格式、大小、输出等进行设置,需要使用到两个指令,分别是access_log和log_format指令。

(1)access_log:用来设置用户访问日志的相关属性。

语法 access_log path[format[buffer=size]]
默认值 access_log logs/access.log combined;
位置 http, server, location

(2)log_format:用来指定日志的输出格式。

语法 log_format name [escape=default/json/none] string….;
默认值 log_format combined “…”;
位置 http

其他配置指令

(1)sendfile:用来设置Nginx服务器是否使用sendfile()传输文件,该属性可以大大提高Nginx处理静态资源的性能

语法 sendfile on/off;
默认值 sendfile off;
位置 http、server、location

(2)keepalive_timeout:用来设置长连接的超时时间。

为什么要使用keepalive?

我们都知道HTTP是一种无状态协议,客户端向服务端发送一个TCP请求,服务端响应完毕后断开连接。
如何客户端向服务端发送多个请求,每个请求都需要重新创建一次连接,效率相对来说比较多,使用keepalive模式,可以告诉服务器端在处理完一个请求后保持这个TCP连接的打开状态,若接收到来自这个客户端的其他请求,服务端就会利用这个未被关闭的连接,而不需要重新创建一个新连接,提升效率,但是这个连接也不能一直保持,这样的话,连接如果过多,也会是服务端的性能下降,这个时候就需要我们进行设置其的超时时间。

语法 keepalive_timeout time;
默认值 keepalive_timeout 75s;
位置 http、server、location

(3)keepalive_requests:用来设置一个keep-alive连接使用的次数。

语法 keepalive_requests number;
默认值 keepalive_requests 100;
位置 http、server、location

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

内功大增!从机械硬盘和固态硬盘的结构来看IO

发表于 2021-11-16

“磁盘”这个词,对于程序员来说并不陌生,我们知道它是一种存储介质,主要用来存储数据的,可以说常用的中间件基本上都离不开它,比如我们常用的MySQL数据库、kafka消息引擎,甚至redis缓存都离不开磁盘。

我们在优化某个业务逻辑的时候,经常需要用到缓存,尽量让热数据都从缓存里读取,因为我们知道磁盘是缓慢的,特别在高并发的场景下,我们要保证极少的请求走磁盘IO。不知道你有没有思考过以下问题:

  1. 机械硬盘为什么慢?
  2. 机械硬盘有多慢?
  3. kafka也是写磁盘的,它却挺快的,为什么?
  4. SSD为什么比普通的机械磁盘要快?
  5. 既然SSD这么快,那为什么不抛弃传统的机械磁盘?

带着这些疑问,我们一起来看看磁盘相关的知识。

从机械硬盘开始


这是一块普通机械硬盘的内部结构,它的组成并不多,我们重点关注磁盘、磁头臂、磁头就行。

先说磁盘,它的样子就像光盘,我们的数据就是存在它里面的,我们其实一般称它为盘片,盘片的表面涂有磁性的记录材料,注意这里不是只有一面可以存数据,盘片的两面都可以存,同时对于一块磁盘来说,通常它是由多个盘片组成的,因此的它组成应该是这样的。


图中一共有4个盘片,一共8个盘面,其中每个盘面都被画成了一圈一圈的同心圆。

这样的一圈一圈的橙色圆我们把它叫做磁道,当然磁道也是由一个个弧段组成的。


像如图的绿色弧段部分我们叫做扇区,扇区是磁盘组成的最小单位,一般一个扇区可以存储512个字节。

多个盘片相同的磁道,可以组成一个虚拟的圆柱,这个虚拟的圆柱面,我们叫做柱面。

对于一个盘面*来说,它的存储容量=单个扇区的大小 \ 磁道的扇区数 * 磁道数。

对于一个盘片*来说,它有两个面所以一个盘片的容量=2 \ 盘面容量。

对于一个磁盘*来说,磁盘的容量=盘片的数量 \ 单盘片的容量。

说完了盘片,我们来说说磁头,我们的数据是在盘片上的,盘片上的数据并不能直接传输到总线上,因此需要一个媒介,这个媒介就是磁头,磁头可以把某个扇区的数据传输到总线上。


说完了磁头,接下来就是磁臂了,磁头解决了数据读写的问题,但是没有解决读写哪个扇区的问题,这时候就需要磁臂,磁臂可以在一定范围内摆动,来找到目标扇区。

当然磁臂的摆动范围有限,比如磁臂无论怎么摆动也无法摆动到扇区B处,这时就该转动盘片了,你应该听过磁盘的转速,比如7200转/分,这个其实就是转轴来带动盘片的转动的速度,因此最终通过盘片的转动+磁臂的摆动,就可以定位到我们的目标扇区。

机械硬盘有多快

搞懂了机械硬盘的物理结构之后,我们接下来看看它有多快。我们知道定位到一条数据需要盘片的转动,还需要磁臂的摆动,这些都是物理的,当我们的磁头定位到具体的扇区之后,读写数据的速度是很快的,因此影响机械硬盘读写速度的主要原因就是这两个物理运动,这两个物理运动对应两个专业名词叫做 平均延时 和 平均寻道时间。

我们先说平均延时,我们上面说到当一个目标扇区不在磁臂的摆动范围之内时,就要转动盘片,以7200转/min的磁盘为例,它每秒可以转动120圈,转一圈就是1/120s=8.33ms左右,那我们定位目标区域的平均耗时是多少呢?以一个磁道为例,它是圆形的,目标节点可以分布在圆上的任何位置。


比如当我们要找A的时候,可能只需要转一点点,找B的时候,可能要转半圈,找C的时候可能要转将近一圈。所以根据算术平均法可以大致判断平均找到一个目标节点需要转动半圈,这个半圈的时间就是8.33/2=4.17ms,也就是平均延时。

我们再来看看平均寻道时间,通过盘面的转动我们大致找到了目标区域,但是还没精确定位到,这时候需要磁臂的摆动去定位我们具体的目标扇区,这个摆动的耗时一般是4-10ms。

因此磁盘随机IO大概的耗时就是4.17+4=8.17 ~ 4.17+10=14.14ms,这个数字代表了什么?我们取个折中,假设随机IO的耗时是10ms,那么1s可以做100次随机IO,看到100这个数字,你是不是明白了什么~,这个是真的小,这也是为什么我们对于QPS较高的接口,都要加个缓存层,因为磁盘扛不住啊。

这里科普下,我们上面说的1s内存能做100次随机IO,这个100我们叫做IOPS,即每秒的输入输出量(或读写次数),是衡量磁盘性能的关键指标

我们可以通过iostat,来查看当前机器磁盘的指标:

1
2
3
bash复制代码iostat
KB/t tps MB/s us sy id 1m 5m 15m
23.44 9 0.20 12 8 80 2.40 1.97 1.90

其中tps就是我们当前磁盘的每秒传输次数,当这个数值很大时需要注意。

当然以上都是随机IO,顺序IO就大大不一样了,顺序IO的速度堪比内存的离散读写,总之很快,像大名鼎鼎的kafka就是磁盘顺序IO,所以至少在磁盘读写这块它的性能还不错。顺序IO之所以快,首先盘片不需要每次转动了,其次我们的磁臂也不需要大幅度的摆动去寻道了,因此节省了大量的物理耗时,速度和随机IO之间应该是数量级的差异。

更加快速的固态硬盘

先说个数字,我们日常用的机械硬盘的数据传输率差不多在200MB/s左右,而固态硬盘的传输率差不多在768MB/s,可以发现固态硬盘比普通机械硬盘快了不少,然而这只是在接口是SATA3.0的情况下,我们的固态硬盘还支持PCI Express接口,在这个接口下,固态硬盘的读写能力还会上一个等级,可以达到1GB/s以上,当然这些只是科普知识,作为程序员的我们不用太在意接口相关的知识。

我们还是先从原理开始讲起,固态硬盘的运作方式和机械磁盘一点都不一样,通过上图的内部结构可以看出,固态硬盘并没有盘片、磁臂等机械部件。

那么它是如何存储数据的呢?答案是电容,电容是非常小的电子元件,我们只需要给电容充上电,那么就可以表示比特位1,给电容放电就可以表示比特位0,采用这样方式存储数据的固体硬盘,我们一般称之为使用了SLC的颗粒,全称是 Single-Level Cell,也就是一个存储单元中只有一位数据,那么一块固态硬盘能存储多少数据就完全取决于能放多少电容,因此后来有的工程师发明了更高级的用法,即让一个电容里能放下2个比特位、3个比特位、甚至4个比特位。

那么问题来了一个电容如何表示这个多的比特位?答案是电压,给电容充电的玩意叫做电压计,以能放两位比特位的电容为例,它可以表示00、01、10、11 4个数字,放电状态是00,其余我们只需要充上不同的电压即可。


当然想要表示的数字越多,就得充很多不同的电压,因此速度就会相对慢些。

短命的固态硬盘

搞懂了固态硬盘的内部结构之后,我们来看看固态硬盘的读写原理,看看为什么固态硬盘的寿命不高。

相比机械硬盘存储数据的盘片,固态硬盘的叫做裸片,裸片之间也是叠放在一起的,以一个裸片为例,它的结构大致如下:


还是划分的概念,首先一张裸片上可以放多个平面,一般一个平面上的存储容量大概是GB级别,然后一个平面上可以分成很多块,一般一个块的存储大小,通常几百KB到几MB大小,一个块里面再就是分很多的页,一个页的大小通常是4KB,我们着重关注下块和页,这和我们接下来要说的固态硬盘的寿命息息相关。

我们知道对于机械硬盘来说,我们写入数据的时候,不会在乎要写入的扇区是否已经有数据,直接覆盖就行了,但是固态硬盘就不一样了,如果某块要写的区域已经有数据了,必须要先擦除,然后才能写入。

这个擦除很关键,因为它就是影响固定硬盘寿命的直接原因,擦的越多寿命就会越来越短,它就像你用一个橡皮擦在一张纸上擦拭一样,擦的多了,纸张也就通了,纸张通了,纸就不能用了。

那么一块固态硬盘可以擦除多少次呢?以单比特电容的模式来说,它大概可以擦除10w次,其他的多比特位的更少,可能只有几千次。因此如果的你业务数据需要经常更新,不太建议使用固态硬盘。

关于擦除数据,还有一点很重要,那就是它是以块为单位的,通过上图我们知道数据存储的最小单位其实是页,页属于块,一个块上有很多的页,如果一个块上的某些页的数据被标记删除了,此时也不能直接擦除这些单独的页,因此这些页就无法被复用,除非整个块上的页都被标记删除了。

如图,页A、页B、页C虽然都是被标记删除的数据,但是因为它所属的块还有其它有效数据,所以当有新数据要写入的时候,它只能写入白色区域未使用的页,并不能利用这些红色区域。但是这样的话,就会出现这样一个问题:随着时间的推移,红色区域会越来越多,也就是碎片越来越多,这样势必会造成浪费。

浪费可耻,因此需要一套紧凑机制,这时候会把相关块的有效数据移动到一个新的块中,让不同块的有效数据更加紧凑的分布,那么对于被移动出数据的块来说,它上面的页要么没数据,要么是标记删除的数据,可以直接对这个块进行擦除,从而达到回收利用的目的。

回到题目

通过对机械硬盘和固态硬盘的了解,我们再来看看一开始的问题:

  1. 机械硬盘为什么慢?机械磁盘慢的原因主要是因为定位一条数据需要盘片的转动 + 磁臂的摆动,这些都是物理的,所以会慢
  2. 机械硬盘有多慢?,通过上面的计算,我们可以大概得出对于一个7200转的机械硬盘来说,它的iops大概在100左右,每次io的耗时在10ms左右
  3. kafka也是写磁盘的,它却挺快的,为什么? 因为kafka是顺序io,就算对于机械硬盘来说,顺序io也是很快的,因为它不会像离散io那样,需要过多的寻道。
  4. SSD为什么比普通的机械硬盘要快?这里主要因为SSD不需要像机械硬盘那样的物理运动来寻道。
  5. 既然SSD这么快,那为什么不抛弃传统的机械硬盘?首先从价格上来讲,固态硬盘价格稍贵,其次固态硬盘的寿命没有机械硬盘的高。

最后

创作不易,各位的三连就是对作者最大的支持,也是作者最大的创作动力,我们下期见。

往期精彩:
  • 内存管理:程序装载那些事
  • 简单!代码原来是这样被CPU跑起来的
  • 20张图!常见分布式理论与解决方案

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SSIS学习使用九:控制流任务错误处理 翻译参考 关于SSI

发表于 2021-11-16

这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战

翻译参考

本文主要参考翻译自 The Stairway to Integration Services 系列文章的 Control Flow Task Errors – Level 9 of the Stairway to Integration Services,目的在于对 SSIS 有一个全面清晰的认识,所有内容在原文的基础上进行实操,由于版本差异、个人疑问等多种原因,未采用完全翻译的原则,同时也会对文中内容进行适当修改,希望最终可以更有利于学习和了解 SSIS,

感谢支持!


在本篇中,我们通过使用 MaximumErrorCount 和 ForceExecutionResult 属性检查任务执行状态管理的方法来介绍容错能力(fault tolerance)。还会学习 SSIS 控制流任务错误(SSIS Control Flow task errors)、事件处理程序(event handlers) 和 容器(containers) 之间的关系。

关于SSIS任务错误

打开 “Precedence.dtsx” 包。看下”序列容器1”,右击 “Script Task 4” 点击 “启用”。

按 “F5” 执行 SSIS 包,询问 “Script Task 4” 的提示框中点击”否”,使其失败。”Script Task 2” 提示框点击 “是”,然后 “Script Task 3” 显示 完成(completed) 消息框。点击”确定”,确认消息框后,界面显示如下:

“Script Task 4” 失败是因为点击“否”按钮。为什么”序列容器1”也失败呢?

事件”冒泡”(Events “Bubble”)

错误是一个事件和事件冒泡。

点击 dtsx 中的 “包资源管理器”(Package Explorer) 标签页,然后展开”Package\可执行文件\序列容器 1\可执行文件\节点”(Package\Executables\Sequence Container 1\Executables\node)

“Script Task 4” 失败并引起一个错误事件。随后将错误事件”向上”(up the line)发送到序列容器1。由于在序列容器1中发生了错误,它也失败了(并变成红色)。错误事件在“范围内”(up in scope)的这种传输有时被称为“冒泡”。

错误事件不会在 序列容器1 处停止;它再次冒泡到 Precedence程序包,该程序包也失败。

从 Package Explorer 树形视图中可视化冒泡的一种方法是想象错误事件”爬树(爬树形视图)”。

错误事件的默认行为是导致任务或容器失败,并在控制流中变为红色的原因。下面看一下如何控制默认行为。

MaximumErrorCount 属性

所有的任务,包括”Script Task 4”,都有一个 MaximumErrorCount 属性。该属性默认值为1,表示一个单个错误将导致任务(task)失败。

你可以将该属性设置为99,当执行包时,”Script Task 4” 失败时,序列容器1仍会失败,没有任何改变。

原因是,MaximumErrorCount属性是设计被容器(containers)使用的,也就是在 任务(task) 上设置 MaximumErrorCount 属性是无效的。

如下,是在 “序列容器1” 上设置 “MaximumErrorCount” 属性为99的效果:

“序列容器1”成功。

通常,一个包在发生错误时需要执行失败。偶尔,也会忽略错误(忽略错误最简单的理由是,忽略错误不会阻塞事件的处理)。通过设置 MaximumErrorCount 属性为0,可以忽略容器错误。

ForceExecutionResult 属性

另一个使”序列容器1”成功的方式是,设置容器的 ForceExecutionResult 属性(默认值为”None”)。

重设 “MaximumErrorCount” 属性为默认值1,修改序列容器1的 “ForceExecutionResult” 属性为”Success”。

调试运行SSIS包,在提示框中设置 “Script Task 4” 失败,”Script Task 2” 成功(失败也可以),最终,”序列容器1” 不会失败。

“序列容器1”成功,即使 “MaximumErrorCount” 属性设置为默认的1,”ForceExecutionResult”属性会覆”MaximumErrorCount”属性。

错误事件(Error Events)

每次一个控制流任务失败,都会引发一个错误事件。

引发错误事件时,错误事件(error event)属性将被填充,并且错误事件触发时,事件消息以静态的方式”沿树向上”传输(transmitted "up the tree")。

创建错误事件处理程序

下面,在 “Script Task 4” 上,通过向 脚本任务(Script Task) 添加 “错误事件处理程序”(OnError Event Handler) 查看一些错误事件属性。

首先,点击控制流中的 “Script Task 4”,然后,点击 “事件处理程序”(Event Handlers) 标签页

“事件处理程序” 选项卡打开,默认显示的是未给在控制流中选择的任务配置 OnError 事件处理程序,如图14所示:

点击可执行文件下拉列表(Executable dropdown),可以导航到其他SSIS包的可执行文件。

在 事件处理程序下拉列表(Event Handler dropdown) 中,可以选择希望创建的 事件处理程序(Event Handler)。

可执行下拉列表 选择 “Script Task 4”,事件处理程序下拉列表 选择 “OnError”,点击下面的 “点击此处为可执行文件“Script Task 4”创建一个“OnError”事件处理程序”(Click here to create an ‘OnError’ event handler for executable ‘Script Task 4’ )

点击链接创建一个OnError事件处理程序,查看下事件处理程序的SSIS工具箱,它也是控制流的工具箱,也就意味着 事件处理程序提供SSIS工作流响应事件。同时事件处理程序也包括一系列事件作用域的变量(这些都是系统变量,需要在”网格选项”中设置显示)。

事件处理程序中使用变量

拖拽一个 脚本任务(Script Task) 到 OnError事件处理程序 界面,打开 脚本任务编辑器,设置 “ScriptLanguage” 属性为 “Microsoft Visual C# 2010”。点击 “ReadOnlyVariables” 属性,然后点击 值文本框 的省略号,在“选择变量窗口”,选择变量 System::ErrorCode、 System::ErrorDescription 和 System::SourceName。

点击“确定”,关闭“选择变量窗口”,脚本页显示如下:

点击 “编辑脚本”(Edit Script) 按钮,打开脚本代码编辑器,Main() 中输入如下内容:

1
2
3
4
5
6
7
8
9
10
11
cs复制代码public void Main()
{
// TODO: Add your code here
var iErrorCode = Convert.ToInt32(Dts.Variables["ErrorCode"].Value);
var sErrorDescription = Dts.Variables["ErrorDescription"].Value.ToString();
var sSourceName = Dts.Variables["SourceName"].Value.ToString();
var sSubComponent = "Script Task 4 OnError Event Handler";
var sMsg = string.Format("Source: {0} \nError Code: {1} \nError Description: {2}", sSourceName,iErrorCode, sErrorDescription);
MessageBox.Show(sMsg, sSubComponent);
Dts.TaskResult = (int)ScriptResults.Success;
}

代码首先创建三个变量 —— iErrorCode、sErrorDescription 和 sSourceName,每个变量获取的是 OnError事件处理程序 相关的 SSIS变量。

获取(或映射)分为两步。第一步设置 ReadOnlyVariables 属性,将 SSIS变量 暴露给脚本任务。第二步是 Dts 命名空间中的 Variables 对象,它可以访问脚本任务 ReadOnlyVariables 属性中列出的变量集合。

在 C# 中获取SSIS变量的语法如下:

1
cs复制代码Dts.Variables["<Variable Name>"].Value

变量名(Variable Name)区分大小写。

Dts.Variables 变量的 Value 属性是一个对象,这意味着我们必须转换对象到其他的数据类型,比如String 或 Integer。

脚本任务错误(Script Task Errors)

如果脚本(C#中)不能在脚本任务的 “ReadOnlyVariables” 属性中找到 SSIS变量,将会抛出类似下面的错误:

错误: 0xC0014054,位于 脚本任务: 无法锁定变量“System::ErrorCod”进行读访问,错误为 0xC0010001“找不到变量。如果在包的执行期间试图从容器的 Variables 集合检索某个变量,但该变量不在此处时,将出现这种情况。变量可能已更改名称,或者并未创建。”

Error: Failed to lock variable “System::ErrorCod” for read access with error 0xC0010001 “The variable cannot be found. This occurs when an attempt is made to retrieve a variable from the Variables collection on a container during execution of the package, and the variable is not there. The variable name may have changed or the variable is not being created.”.

上面的错误可以通过删除 脚本任务的”ReadOnlyVariables”属性 中列出的 System::ErrorCode 变量名中的”e”产生。通过右击”脚本任务”并点击”执行任务”执行。

任务(Task)执行失败,错误消息也可以在 “进度”(Progress,或执行结果 —— Execution Results) 标签页中看到

These errors are tedious to troubleshoot.
这些错误很难解决。(这些错误进行故障排除是很乏味的)

更正脚本任务属性中”ErrorCode”的拼写。一个快速的单元测试(unit test) —— 在OnError事件处理程序中执行 “脚本任务”,应该会出现类似下面的结果

现在已经准备好测试 “Precedence.dtsx” SSIS包中的错误。

观察错误(Observing an Error)

执行前面的 SSIS 包,”Script Task 4” 提示框中点击”否”,这样 “Script Task 4” 失败并引发一个错误事件。这个错误事件被 OnError事件处理程序(OnError event handler) “监听者” “听到”,导致 事件处理程序 中的”脚本任务”执行并显示错误信息。如下:

更多关于冒泡(Bubbling)

之前,我们提到事件“冒泡”。在这种情况下,在 “Script Task 4” 生成的Error事件将在SSIS包的范围内 “向上传输”(“up the tree”..to) 到 “序列容器1”。如果我们为 序列容器1 配置OnError事件处理程序,我们可以观察到这种情况的发生。

如果调试器正在运行则停止。

在 控制流 中,选择”序列容器1”,点击 “事件处理程序” 标签页。和之前一样,点击链接,为”序列容器1”创建一个OnError事件处理程序。从 “Script Task 4” 的OnError事件处理程序中复制 “脚本任务”,粘贴到 “序列容器1” 的 OnError事件处理程序 中。

打开粘贴的”脚本任务”编辑并点击”编辑脚本”按钮。修改 Main 函数中如下行:

1
cs复制代码var sSubComponent = "序列容器1 OnError Event Handler";

注:暂时无法获取绑定当前事件的 task 的名字,测试了多种方法均不行,目前能获取的是 源task 名字(发生错误的任务,事件冒泡时就无法获知绑定事件处理程序的任务名了)、当前任务的名字(即当前事件处理程序中的”脚本任务”的名字)

右击当前的 “脚本任务”,点击 “执行任务”,会看到如下的执行结果。

通过 调试器执行 进入 SSIS包,”Script Task 4” 询问成功的提示框中点击 “否”,生成来自 “Script Task 4” 的错误事件。事件处理程序作为一个事件的监听者执行它的函数 —— “听到”并响应 “Script Task 4” 错误事件。

可以很方便的从消息框的 Title标题 中分辨出它是 “Script Task 4” 的 OnError事件处理程序。

点击”确定”按钮,确认消息框。错误事件然后冒泡到 “序列容器1” 的 OnError事件处理程序中

同样,你可以从消息框的 title标题 中分辨出它是 “序列容器1” 的事件处理程序。

注意消息框的内容,错误事件的源是 “Script Task 4”,并且 “Error Description” 和 “Error Code” 都没有改变。对于 SSIS 中的事件,这是非常有趣的行为。当SSIS任务最初引发事件时,将填充事件属性。一旦将事件放置在 消息总线(messaging bus) 上,这些值就不会更改。

该事件将继续从 “序列容器1” 冒泡到 “Precedence.dtsx” 包。同时 变量值 将保持静态,通知任何关于此错误配置的监听器。

总结

本篇中,我们查看了 SSIS控制流 任务错误行为,包括 错误事件(Error events)、OnError事件处理程序(OnError event handlers) 和 错误冒泡(error bubbling)。

同时还了解了事件冒泡和容器之间的关系,也介绍了 MaximumErrorCount 和 ForceExecutionResult 属性的容错能力。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

k8s网络模型与集群通信

发表于 2021-11-16

在k8s中,我们的应用会以pod的形式被调度到各个node节点上,在设计集群如何处理容器之间的网络时是一个不小的挑战,今天我们会从pod(应用)通信来展开关于k8s网络的讨论。

小作文包含如下内容:

  • k8s网络模型与实现方案
  • pod内容器通信
  • pod与pod通信
  • pod与service通信
  • 外网与service通信

k8s网络模型与实现方案

k8s集群中的每一个Pod(最小调度单位)都有自己的IP地址,即ip-per-pod模型。

在ip-per-pod模型中每一个pod在集群中保持唯一性,我们不需要显式地在每个 Pod 之间创建链接, 不需要处理容器端口到主机端口之间的映射。从端口分配、命名、服务发现、 负载均衡、应用配置和迁移的角度来看,Pod 可以被视作独立虚拟机或者物理主机。

如下图,从表面上来看两个容器在docker网络与k8s网络中与client通信形式。

ip-per-pod

k8s是一套庞大的分布式系统,为了保持核心功能的精简(模块化)以及适应不同业务用户的网络环境,k8s通过CNI(Container Network Interface)即容器网络接口集成各种网络方案。这些网络方案必须符合k8s网络模型要求:

  • 节点上的 Pod 可以不通过 NAT 和其他任何节点上的 Pod 通信
  • 节点上的代理(比如:系统守护进程、kubelet)可以和节点上的所有Pod通信

备注:仅针对那些支持 Pods 在主机网络中运行的平台(比如:Linux):

  • 那些运行在节点的主机网络里的 Pod 可以不通过 NAT 和所有节点上的 Pod 通信

如此操作,是不是有点像美团?将配送业务外包(CNI)给三方公司(实现方案),骑手是通过哪种飞机大炮(网络)送餐的我不管,只要符合准时、不撒漏(模型要求)等相关规矩这就是一次合格的配送。

CNI 做两件事,容器创建时的网络分配,和当容器被删除时释放网络资源。 常用的 CNI 实现方案有 Flannel、Calico、Weave以及各种云厂商根据自身网络推出的CNI插件如华为的 CNI-Genie、阿里云Terway。关于各实现方案的原理不是本次讨论重点,有机会单独写一篇。


pod内容器通信

Pod内容器非常简单,在同一个 Pod 内,所有容器共享存储、网络即使用同一个 IP 地址和端口空间,并且可以通过 localhost 发现对方。Pod 使用了一个中间容器 Infra,Infra 在 Pod 中首先被创建,而其他容器则通过 Join Network Namespace 的方式与 Infra 容器关联在一起。

pod-network

我们有一个pod包含busybox、nginx这两个容器

1
2
3
sql复制代码kubectl get pod -n training
NAME                             READY   STATUS   RESTARTS   AGE
pod-localhost-765b965cfc-8sh76   2/2     Running   0         2m56s

在busybox中使用telnet连接nginx容器的 80端口看看。

1
2
3
4
bash复制代码kubectl exec -it  pod-localhost-765b965cfc-8sh76 -c container-si1nrb -n training -- /bin/sh
​
# telnet localhost 80
Connected to localhost

一个pod有多个容器时可以通过-c指定进入的容器名(通过describe查看容器名称),显然通过localhost就可以轻松访问到同一个pod中的nginx容器80端口。这也是在许多关系密切的应用中通常会部署在同一个pod中。


pod与pod通信

  1. pod在同一主机

我们通过node选择器将两个pod调度到同一个node中

1
2
3
4
yaml复制代码 ...
nodeSelector:
      kubernetes.io/hostname: node2
...

两个容器分别获得一个IP地址,同样通过IP地址双方网络正常互通。

1
2
3
4
5
6
7
8
9
sql复制代码# kubectl get pod -o wide -n training 
NAME                                 READY   STATUS   RESTARTS   AGE     IP             NODE                   NOMINATED NODE   READINESS GATES
​
pod-to-pod-64444686ff-w7c4g           1/1     Running   0         6m53s   100.82.98.206   node2       <none>           <none>
pod-to-pod-busybox-7b9db67bc6-tl27c   1/1     Running   0         5m3s    100.82.98.250   node2       <none>           <none>
# kubectl exec -it pod-to-pod-busybox-7b9db67bc6-tl27c -n training -- /bin/sh
/# telnet 100.82.98.206 80
Connected to 100.82.98.206
​

同一主机网络的pod互通和我们之前学习的docker bridge相似,通过linux网桥添加虚拟设备对veth pair连接容器和主机主机命名空间。具体可查看文章《docker容器网络bridge》。

我们把之前的图拿过来,在k8s中只不过把灰色部分替换成CNI方案实现。

CNI-B

  1. pod在不同主机

此时我们的pod分布如下:

1
2
3
4
5
6
7
8
9
10
sql复制代码kubectl get pod -o wide -n training 
NAME                                       READY   STATUS   RESTARTS   AGE   IP             NODE                   NOMINATED NODE   READINESS GATES
​
pod-to-pod-64444686ff-w7c4g                 1/1     Running   0         104m   100.82.98.206   node2       <none>          
​
pod-to-pod-busybox-node2-6476f7b7f9-mqcw9   1/1     Running   0         42s    100.91.48.208   node3       <none>    
​
# kubectl exec -it pod-to-pod-busybox-node2-6476f7b7f9-mqcw9 -n training -- /bin/sh
/ # telnet 100.82.98.206 80
Connected to 100.82.98.206

pod在不同主机的通信依赖于CNI插件,这里我们以Calico为例的做简单了解,从Calico架构图中可以看到每个node节点的自身依然采用容器网络模式,Calico在每个节点都利用Linux 内核实现了一个高效的虚拟路由器vRouter来负责数据转发。每个虚拟路由器将路由信息广播到网络中,并添加路由转发规则。同时基于iptables还提供了丰富的网络策略,实现k8s的Network Policy策略,提供容器间网络可达性限制的功能。

简单理解就是通过在主机上启动虚拟路由器(calico node),将每个主机作为路由器使用实现互联互通的网络拓扑。

Calico节点组网时可以直接利用数据中心的网络结构(L2或者L3),不需要额外的NAT、隧道或者Overlay Network,没有额外的封包解包,能够节约CPU运算,提高网络效率。

calico


pod与service通信

我们知道在k8s中容器随时可能被摧毁,pod的IP显然不是持久的,会随着扩展或缩小应用规模、或者应用程序崩溃以及节点重启等而消失和出现。service 设计就是来处理这个问题。service可以管理一组 Pod 的状态,允许我们跟踪一组随时间动态变化的 Pod IP 地址。而客户端只需要知道service这个不变的虚拟IP就可以了。

我们先来看看典型的service与pod使用,我们创建了一个service,标签选择器为app:nginx,将会路由到app=nginx标签的Pod上。

service

1
2
3
scss复制代码# kubectl get service -n training
NAME               TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
training-service   ClusterIP   10.96.229.238   <none>        8881/TCP   10m

Service对外暴露的端口8881,这样在集群的中的pod即可通过8881访问到与service 绑定的label为app=nginx的pod

1
2
3
4
5
6
7
8
xml复制代码kubectl run -it --image nginx:alpine curl --rm /bin/sh
/ # curl 10.96.229.238:8881
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
...

其实大多数时候在自动化部署服务时并不知道service ip,所以另一种常见方式通过DNS进行域名解析后,可以使用 “ServiceName:Port” 访问Service,可以自己尝试一下。

service 是如何做到服务发现的?

Endpoints是k8s中的一种资源对象,k8s通过Endpoints监控到Pod的IP,service又关联Endpoints从而实现Pod的发现。大致如下图所示,service的发现机制我们会在后面文章中做深入了解。

endpoints


外网与service通信

其实所谓外网通信也是service的表现形式。

service几种类型和不同用途。

  • ClusterIP:用于在集群内部互相访问的场景,通过ClusterIP访问Service,即我们上面所说的pod与service。
  • NodePort:用于从集群外部访问的场景,通过节点上的端口访问Service。
  • LoadBalancer:用于从集群外部访问的场景,其实是NodePort的扩展,通过一个特定的LoadBalancer访问Service,这个LoadBalancer将请求转发到节点的NodePort,而外部只需要访问LoadBalancer。
  • None:用于Pod间的互相发现,这种类型的Service又叫Headless Service。

我们先来看NodePort:

service-nodeport

我们在service中指定type: NodePort创建出的service将会包含一个在所有node 开放的端口30678,这样我们访问任意节点IP:30678即可访问到我们的pod

1
2
3
4
5
6
7
8
9
10
11
xml复制代码# kubectl get service -n training
NAME               TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)         AGE
training-service   NodePort   10.96.229.238   <none>        8881:30678/TCP   55m
​
# curl 192.168.1.86:30678
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
....

LoadBalancer类型和它名字一样,为负载均衡而生。它的结构如下图所示,

loadbalancer

LoadBalancer本身不是属于Kubernetes的组件,如果使用云厂商的容器服务。通常会提供一套他们的负载均衡服务比如阿里云ACK的SLB、华为云的ELB等等。Service是基于四层TCP和UDP协议转发的,而k8s 另外一种资源对象Ingress可以基于七层的HTTP和HTTPS协议转发,可通过域名和路径做到更细粒度的划分,这是后话。


希望小作文对你有些许帮助,如果内容有误请指正。

您可以随意转载、修改、发布本文,无需经过本人同意。 通过博客阅读:iqsing.github.io

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

排查not eligible for getting pro

发表于 2021-11-16

前言

上一篇文章我们聊了一下自定义实现的SPI如何与spring进行整合,但其实在实现的过程中有个小细节,就是原先我们的SPI是带了拦截器功能,(ps:对如何实现一个带拦截器的SPI感兴趣的朋友,可以查看这篇文章–>聊聊如何实现一个带有拦截器功能的SPI)。

为了保留这个拦截器功能,我原先的想法是狸猫换太子,在spring提供的后置处理器里面,把拦截器功能给整合进去,当时的实现代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@Slf4j
@Deprecated
public class SpiInstancePostProcessor implements BeanPostProcessor {

private DefaultListableBeanFactory beanFactory;

private InterceptorHandler interceptorHandler;

public SpiInstancePostProcessor(InterceptorHandler interceptorHandler,DefaultListableBeanFactory beanFactory) {
this.interceptorHandler = interceptorHandler;
this.beanFactory = beanFactory;
}


@SneakyThrows
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if(bean.getClass().isAnnotationPresent(Activate.class)){
return interceptorHandler.getInterceptorChain().pluginAll(bean);
}
return bean;
}

}

功能是实现了,但是控制台却出现如下信息

trationDelegate$BeanPostProcessorChecker : Bean ‘interceptorHandler’ of type [com.github.lybgeek.spring.interceptor.handler.InterceptorHandler] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

排查过程

当时排查是通过控制提示的信息,找到对应源码。在

1
java复制代码org.springframework.context.support.PostProcessorRegistrationDelegate

找到相应的实现

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (!(bean instanceof BeanPostProcessor) && !isInfrastructureBean(beanName) &&
this.beanFactory.getBeanPostProcessorCount() < this.beanPostProcessorTargetCount) {
if (logger.isInfoEnabled()) {
logger.info("Bean '" + beanName + "' of type [" + bean.getClass().getName() +
"] is not eligible for getting processed by all BeanPostProcessors " +
"(for example: not eligible for auto-proxying)");
}
}
return bean;
}

看到这个信息,按正常做法是让

1
2
java复制代码!(bean instanceof BeanPostProcessor) && !isInfrastructureBean(beanName) &&
this.beanFactory.getBeanPostProcessorCount() < this.beanPostProcessorTargetCount

这个语句块为false,从代码我们很容易看出语句块为false,有几个入口

  1. !(bean instanceof BeanPostProcessor)
  2. !isInfrastructureBean(beanName)
  3. this.beanFactory.getBeanPostProcessorCount() <

this.beanPostProcessorTargetCount

1和3看起来是没多大发挥空间,我们可以看下2,2的代码块如下

1
2
3
4
5
6
7
8
java复制代码	private boolean isInfrastructureBean(@Nullable String beanName) {
if (beanName != null && this.beanFactory.containsBeanDefinition(beanName)) {
BeanDefinition bd = this.beanFactory.getBeanDefinition(beanName);
return (bd.getRole() == RootBeanDefinition.ROLE_INFRASTRUCTURE);
}
return false;
}
}

从代码我们可以看出

1
java复制代码bd.getRole() == RootBeanDefinition.ROLE_INFRASTRUCTURE

这句话是核心,然而ROLE_INFRASTRUCTURE这个又是什么鬼,我们继续跟踪

1
2
3
4
5
6
7
java复制代码	/**
* Role hint indicating that a {@code BeanDefinition} is providing an
* entirely background role and has no relevance to the end-user. This hint is
* used when registering beans that are completely part of the internal workings
* of a {@link org.springframework.beans.factory.parsing.ComponentDefinition}.
*/
int ROLE_INFRASTRUCTURE = 2;

这句话表达的意思是当声明这个角色时,这个bean就不是归属外部用户,而是归属spring内部。换句话说就是这个bean是个官方bean,不是群众bean。总之这个就是一个身份标识,因此我们在把bean注入到spring容器中,就可以做如下处理

1
2
3
4
5
java复制代码    @Bean
@Role(RootBeanDefinition.ROLE_INFRASTRUCTURE)
public InterceptorHandler interceptorHandler(final ObjectProvider<List<Interceptor>> listObjectProvider) {
return new InterceptorHandler(interceptorChain(listObjectProvider));
}

当加上

1
java复制代码 @Role(RootBeanDefinition.ROLE_INFRASTRUCTURE)

这个注解后,世界果然清净了,控制台再也没有出现

1
java复制代码not eligible for getting processed by all BeanPostProcessors

但问题真的就解决了吗?

答案见仁见智,很多时候我们很容易欺骗一个人一时,但却很难欺骗这个人一世,好比如你告诉妹子我家财万贯,妹子相信了,等到妹子要你给她买一些贵重的东西时,发现你买不起,你为了讨妹子欢心,不得以打肿脸充胖子,提前消费你压根买不起的东西,导致你后面没法再消费其他你本可以消费的东西

在spring世界也是如此,BeanPostProcessor本身也是一个Bean,一般而言其实例化时机要早过普通的Bean,但是她现在对于你实现的bean有一些需求,即BeanPostProcessor有时也会依赖一些Bean,这就导致了一些普通Bean的实例化早于BeanPostProcessor的可能情况,而引发一些情况,比如这些提前初始化的bean无法享有一些后置处理器扩展的功能

因此对本文的案例,用

1
java复制代码 @Role(RootBeanDefinition.ROLE_INFRASTRUCTURE)

其实是没解决问题本质,但是因为本文的拦截器不需要后续的一些spring特色功能,因此这种解法也算是一种吧。

那有没有其他解法,答案是有的,我们可以利用

1
java复制代码org.springframework.beans.factory.SmartInitializingSingleton

这个类,他这个类里面有个

1
java复制代码afterSingletonsInstantiated()

方法,这个方法的作用是在所有单例bean初始化完成调用后的回调接口。本文后面的例子就是改用这个接口实现,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class SpiInstanceInitializingSingleton implements SmartInitializingSingleton,BeanFactoryAware {

private DefaultListableBeanFactory beanFactory;

private InterceptorHandler interceptorHandler;

public SpiInstanceInitializingSingleton(InterceptorHandler interceptorHandler) {
this.interceptorHandler = interceptorHandler;
}

@Override
public void afterSingletonsInstantiated() {
changeBeanInstance2ProxyInstance();

}
}

当然还可以使用spring的监听机制,比如监听refresh事件进行处理

总结

本文算是自定义实现的SPI如何与spring进行整合这篇文章的一点扩展补充

demo链接

github.com/lyb-geek/sp…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…329330331…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%