2023年12月

一:背景

1. 讲故事

在dump分析的旅程中也会碰到一些让我无法解释的灵异现象,追过这个系列的朋友应该知道,上一篇我聊过
宇宙射线
导致的程序崩溃,后来我又发现了一例,而这一例恰恰是高铁的
列控连锁一体化
程序,所以更加让我确定这是由于
电离辐射
干扰了计算机的
数字信号
导致程序的bit翻转,而这一篇也是一个我认为的
灵异现象
,拿出来给朋友们分享一下。

前段时间有位朋友找到我,说他的程序会偶发性崩溃,一直找不到原因很纠结,看我在这一块非常有经验让我帮忙看一下怎么回事,既然是有备而来自然dump也准备好了,接下来开始分析之旅吧。

二:WinDbg 分析

1. 为什么会崩溃

要想分析崩溃的原因还得windbg自带的自动化分析命令
!analyze -v
,输出如下:


0:117> !analyze -v
*******************************************************************************
*                                                                             *
*                        Exception Analysis                                   *
*                                                                             *
*******************************************************************************

CONTEXT:  (.ecxr)
rax=0000000000000001 rbx=0000000000000000 rcx=0000000000000002
rdx=000000000005001b rsi=000000000000000e rdi=00000161b1b8c718
rip=00007ffdd0961abd rsp=000000341547b370 rbp=000000341547b250
 r8=0000000000000005  r9=000000000000003d r10=0000000000000000
r11=7007f0b8d350316a r12=0000000000000000 r13=0000000000000003
r14=000000341547b5c0 r15=0000000000000001
iopl=0         nv up ei pl nz na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
clr!_report_gsfailure+0x1d:
00007ffd`d0961abd cd29            int     29h
Resetting default scope

EXCEPTION_RECORD:  (.exr -1)
ExceptionAddress: 00007ffdd0961abd (clr!_report_gsfailure+0x000000000000001d)
   ExceptionCode: c0000409 (Security check failure or stack buffer overrun)
  ExceptionFlags: 00000001
NumberParameters: 1
   Parameter[0]: 0000000000000002
Subcode: 0x2 FAST_FAIL_STACK_COOKIE_CHECK_FAILURE 

SYMBOL_NAME:  clr!_report_gsfailure+1d

...

卦中有一句话叫
Security check failure or stack buffer overrun
,浅层意思就是:
安全检查失败或缓冲区溢出
,行话就是:栈上的cookie遭到了破坏。

可能有些朋友对 cookie 不是很了解,这个cookie非web的cookie,而是在方法栈上藏的一个随时值,在方法的退出前会检查这个值有没有被破坏,目的就是防止有人无意或者恶意攻击线程栈,如果遭到破坏,会触发
int 29

nt!KiRaiseSecurityCheckFailure
函数让程序快速硬性崩溃。

如果有些朋友不明白,画个图如下:

既然说 cookie 被破坏了,说明有栈溢出的情况,那到底溢出了什么东西呢?这需要分析崩溃处附近的汇编代码才能知道,接下来使用
.ecxr ; k 3
切到崩溃前的上下文。


0:117> .ecxr ; k 3
rax=0000000000000001 rbx=0000000000000000 rcx=0000000000000002
rdx=000000000005001b rsi=000000000000000e rdi=00000161b1b8c718
rip=00007ffdd0961abd rsp=000000341547b370 rbp=000000341547b250
 r8=0000000000000005  r9=000000000000003d r10=0000000000000000
r11=7007f0b8d350316a r12=0000000000000000 r13=0000000000000003
r14=000000341547b5c0 r15=0000000000000001
iopl=0         nv up ei pl nz na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
clr!_report_gsfailure+0x1d:
00007ffd`d0961abd cd29            int     29h
 # Child-SP          RetAddr               Call Site
00 00000034`1547b370 00007ffd`d0977900     clr!_report_gsfailure+0x1d
01 00000034`1547b3b0 00007ffd`d097816d     clr!RtlAllocateLUnicodeString+0xe0
02 00000034`1547b420 00007ffd`d09e1d06     clr!RtlDuplicateLUnicodeString+0x8d
...

卦中的信息很丰富,说 clr 在
RtlAllocateLUnicodeString
函数退出阶段时检查 cookie 被破坏了,继而程序快速崩溃,接下来需要反编译
RtlAllocateLUnicodeString
函数,简化后如下:


0:117> uf clr!RtlAllocateLUnicodeString
clr!RtlAllocateLUnicodeString:
00007ffd`d0977820 48895c2418      mov     qword ptr [rsp+18h],rbx
00007ffd`d0977825 55              push    rbp
00007ffd`d0977826 56              push    rsi
00007ffd`d0977827 57              push    rdi
00007ffd`d0977828 488bec          mov     rbp,rsp
00007ffd`d097782b 4883ec50        sub     rsp,50h
00007ffd`d097782f 488b05d2777600  mov     rax,qword ptr [clr!_security_cookie (00007ffd`d10df008)]
00007ffd`d0977836 4833c4          xor     rax,rsp
00007ffd`d0977839 488945f8        mov     qword ptr [rbp-8],rax
00007ffd`d097783d 488bfa          mov     rdi,rdx
00007ffd`d0977840 488bf1          mov     rsi,rcx
00007ffd`d0977843 c745f0e50000c0  mov     dword ptr [rbp-10h],0C00000E5h
00007ffd`d097784a 33db            xor     ebx,ebx
00007ffd`d097784c 4885d2          test    rdx,rdx
00007ffd`d097784f 745f            je      clr!RtlAllocateLUnicodeString+0x90 (00007ffd`d09778b0)  Branch
...
00007ffd`d09778f2 8bc3            mov     eax,ebx
00007ffd`d09778f4 488b4df8        mov     rcx,qword ptr [rbp-8]
00007ffd`d09778f8 4833cc          xor     rcx,rsp
00007ffd`d09778fb e820a1feff      call    clr!_security_check_cookie (00007ffd`d0961a20)
00007ffd`d0977900 488b9c2480000000 mov     rbx,qword ptr [rsp+80h]
00007ffd`d0977908 4883c450        add     rsp,50h
00007ffd`d097790c 5f              pop     rdi
00007ffd`d097790d 5e              pop     rsi
00007ffd`d097790e 5d              pop     rbp
00007ffd`d097790f c3              ret

卦中的信息量还是非常大的,我们通读下汇编代码理解下
安全检查
中的一些基本元素以及逻辑是什么? 步骤大概如下:

  1. _security_cookie

这个是 cookie 种子,可以用 dp 给捞出来,即下面的
0000d9998c879750


0:117> dp clr!_security_cookie L1
00007ffd`d10df008  0000d999`8c879750

  1. xor rax,rsp

将 cookie 种子和当前方法的栈顶指针rsp异或一下,目的就是做一个和栈帧相关的随机值,当前的rsp即k上的
000000341547b3b0
,用 windbg 计算之后为:


0:117> ? 00000034`1547b3b0 ^ 0000d999`8c879750
Evaluate expression: 239339632076000 = 0000d9ad`99c024e0

  1. qword ptr [rbp-8],rax

将异或后的
安全值
塞到
rbp-8
的栈位置,这里的 rbp 由上面的汇编语句
mov rbp,rsp
赋值的,因为上面有三个push加一个call,所以rbp应该退掉4个0x8,最后计算的结果为栈位置000000341547b3f8 存的就是安全值,下面的输出也可以确认。


0:117> ? 00000034`1547b420-0x8-0x8-0x8-0x8
Evaluate expression: 223695320064 = 00000034`1547b400

0:117> dp 00000034`1547b400-8 L1
00000034`1547b3f8  0000d9ad`99c024e0

  1. clr!_security_check_cookie

在方法退出时需要通过
_security_check_cookie
方法来检查cookie是否损坏,核心代码为:


clr!RtlAllocateLUnicodeString+0xd2:
00007ffd`d09778f4 488b4df8        mov     rcx,qword ptr [rbp-8]
00007ffd`d09778f8 4833cc          xor     rcx,rsp
00007ffd`d09778fb e820a1feff      call    clr!_security_check_cookie (00007ffd`d0961a20)

经过 windbg 计算
rcx=0000d9998c879750
,即
_security_cookie
值。


0:117> dp 00000034`1547b400-8 L1
00000034`1547b3f8  0000d9ad`99c024e0

0:117> ? 0000d9ad`99c024e0 ^ 00000034`1547b3b0
Evaluate expression: 239253510920016 = 0000d999`8c879750

接下来拿着
rcx= 0000d9998c879750
去反汇编下 _security_check_cookie 函数,简化后如下:


0:117> uf clr!_security_check_cookie
00007ffd`d0961a20 483b0de1d57700  cmp     rcx,qword ptr [clr!_security_cookie (00007ffd`d10df008)]
00007ffd`d0961a27 7510            jne     clr!_security_check_cookie+0x19 (00007ffd`d0961a39) 
00007ffd`d0961a29 48c1c110        rol     rcx,10h
00007ffd`d0961a2d 66f7c1ffff      test    cx,0FFFFh
00007ffd`d0961a32 7501            jne     clr!_security_check_cookie+0x15 (00007ffd`d0961a35) 
00007ffd`d0961a34 c3              ret
00007ffd`d0961a35 48c1c910        ror     rcx,10h
00007ffd`d0961a39 e962000000      jmp     clr!_report_gsfailure (00007ffd`d0961aa0) 
00007ffd`d0961aa0 48894c2408      mov     qword ptr [rsp+8],rcx
00007ffd`d0961aa5 4883ec38        sub     rsp,38h
00007ffd`d0961aa9 b917000000      mov     ecx,17h
00007ffd`d0961aae ff15e4fa5a00    call    qword ptr [clr!_imp_IsProcessorFeaturePresent (00007ffd`d0f11598)]
00007ffd`d0961ab4 85c0            test    eax,eax
00007ffd`d0961ab6 7407            je      clr!_report_gsfailure+0x1f (00007ffd`d0961abf) 
00007ffd`d0961ab8 b902000000      mov     ecx,2
00007ffd`d0961abd cd29            int     29h

代码逻辑非常简单,还原成 C 大概如下:


void __fastcall _security_check_cookie(uintptr_t stackcookie)
{
	if ((stackcookie == __security_cookie) && (stackcookie高四位 == "0000")) {
		return;
	}
	else {
		_report_gsfailure()
	}
}

从C的逻辑看我们的
stackcookie=0000d9998c879750
完全满足 if 条件,但不知道为什么会走到这个 else 里面去,无法想象。。。所以定性为 灵异事件!!!

4. 故事后续

把所有的值都推算完了之后,在不可能走到 else 的情况下还是走到了 else,这个真的很让人无语+费解,过了几天找朋友确认的时候,朋友又反馈了一个信息,说电脑上的
其他程序
也会遇到这种情况,让客户重装操作系统,目前还没遇到问题。

所以我觉得这个问题可能是
操作系统层面
的问题,或者是
硬件层面
的问题,而且程序的异常是在 clr 层面,用户代码是无法干涉的,程序中也没有做 Pinvoke。

三:总结

一个是辐射导致的bit位翻转,一个是不可能走到else的地方走了else,各个奇奇怪怪的事情,让我的
高级调试之旅
丰富多彩,大家觉得这个崩溃还有其他的可能性吗?期待大家的留言。

图片名称

引言

说到颜色,前端的小伙伴们一定都不陌生,比如字体颜色、背景色等等,颜色是构建视觉效果的重要部分,所以也必然是可视化的关键部分,当学习到可视化中有关于颜色表示的这一部分时,我也想起了我曾经玩过的一个游戏,叫做Blendoku,这个名字和数独的Sudoku有些类似,考验的是玩家对颜色的敏锐度,下面是其中一个关卡的截图,可以明显看出,这个截图中有一个颜色渐变的趋势。

blendoku

色彩对人的视觉感知以及情绪心理都存在不少的影响,所以了解颜色表示对可视化非常重要。那么图形系统中都有哪些颜色表示方式呢?

RGB/RGBA

我想很多人应该和我一样,对于RGB和RGBA的色值形式是最熟悉的,对我来说,其他的颜色表示方式用的很少,了解的也很少,HSL还略有所耳闻,但是对于CIE Lab、Cubehelix这些,在学习可视化前,我甚至都没怎么听说过,当我们拿到一份设计稿试图去还原页面时,首选的色值基本都是RGB/RGBA的表示形式。它使用起来非常简单,也很好理解,RGB三个字母分别代表了Red、Green、Blue,也就是红、绿、蓝三个颜色通道的色阶,色阶代表了某个通道的强弱。

RGB有两种写法,一种是十六进制的形式,另一种是rgb/rgba函数的形式。在十六进制形式中,使用两位数来表示某一通道的色阶,最小能表示的值是00,最大能表示的值为FF,转换为十进制就是0到255,因此每个通道分别有256阶。

我们可以用一个三维立方体,把RGB能表示的所有颜色描述出来。就如下图所示:

rgb1

根据此图显而易见,RGB色值并不能表示人眼可见的所有颜色;但就平常的使用而言,也足够丰富了,大多数设备,比如一般的显示器、彩色打印机、扫描仪等等,都支持RGB的颜色表示。

RGBA则是在RGB的基础上增加了一个对应透明度的alpha通道。

对于一般的网页开发而言,RGB/RGBA的使用并没什么太大的问题,但是如果用于数据可视化方面的开发,就存在比较明显的短板。

比如需要根据数据生成一组对比明显的颜色,来进行图表的展示,但实际上
从RGB的色值上,我们并不能得到关于两个颜色的实际差异
,也就是说,两个色值之间的差值,只能反映出它们在RGB立方体中的相对距离。

比如下面这个例子:

我们在画布上生成3组颜色不同的圆,每组5个圆;颜色使用随机生成。

import { Vec3 } from 'https://unpkg.com/ogl';
// ...
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.scale(1, -1); // 绕X轴翻转

for (let i = 0; i <  3; i ++) {
  const colorVector = randomRGB();
  for (let j = 0; j < 5; j ++) {
    const c = colorVector.clone().scale(0.5 + 0.25 * j);
    ctx.fillStyle = `rgb(${Math.floor(c[0] * 256)}, ${Math.floor(c[1] * 256)}, ${Math.floor(c[2] * 256)})`;
    ctx.beginPath();
    ctx.arc((j - 2) * 60, (i - 1) * 60, 20, 0, Math.PI * 2);
    ctx.fill();
  }
}

function randomRGB() {
  return new Vec3(
      0.5 * Math.random(),
      0.5 * Math.random(),
      0.5 * Math.random(),
  )
}
  • 首先我们生成随机的三维向量
    colorVector
    ,用于后续构建RGB颜色,
    0.5 * Math.random()
    使得每个分量的范围都是
    [0, 0.5)
  • 然后我们在每一组圆上,依次用0.5、0.75、1.0、1.25和1.5的比率乘以随机生成的三维向量,再通过乘以256,就得到了一个随机的RGB色值

这样,一组圆就能呈现不同的亮度;总体上,越左边越暗,越右边越亮。但我们能发现,这样子生成的随机RGB颜色存在两个缺点:

  1. 行与行之间的颜色差别可能很大,也可能很小
  2. 我们无法控制随机生成的颜色本身的亮度,一组圆的颜色可能都很亮或者都很暗,区分度差

总的来说,就是随机生成的RGB颜色彼此之间的
区分度不能保证
;因此,在需要动态构建颜色时,很少直接用RGB色值,而是使用其他的颜色表示形式;其中比较常用的就是HSL和HSV颜色表示形式。

HSL/HSV

HSL和HSV用色相(Hue)、饱和度(Saturation)和亮度(Lightness)或明度(Value)来表示颜色。

其中,Hue是角度,取值范围是0到360度,饱和度和亮度/明度的取值都是从0到100%。

虽然HSL和HSV有一些区别,但实现的效果比较接近。

简单来说,我们可以把HSL和HSV理解为,是将RGB颜色的立方体从直角坐标系投影到极坐标的圆柱上,所以它的色值和RGB色值是一一对应的。可以参考下图:

hsl1

它们之间互相转换的算法比较复杂。CSS和Canvas2D可以直接支持HSL颜色,只有WebGL中需要我们自己去转换,一般而言直接使用一些现有的转换代码就足够了,如果有对这个实现算法感兴趣的小伙伴,可以自己去深入研究一下。

现在我们用HSL颜色改写前面三排圆的例子,同样也是随机生成颜色:

import {Vec3} from 'https://unpkg.com/ogl';
// ...
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.scale(1, -1); // 绕X轴翻转

const [h, s, l] = randomColor();
for (let i = 0; i <  3; i ++) {
  const p = (i * 0.25 + h) % 1;
  for (let j = 0; j < 5; j ++) {
    const d = j - 2;
    ctx.fillStyle = `hsl(${Math.floor(p * 360)}, ${Math.floor((0.15 * d + s) * 100)}%, ${Math.floor((0.12 * d + l) * 100)}%)`;
    ctx.beginPath();
    ctx.arc((j - 2) * 60, (i - 1) * 60, 20, 0, Math.PI * 2);
    ctx.fill();
  }
}

function randomColor() {
  return new Vec3(
      0.5 * Math.random(), // 色相:0~0.5之间的值
      0.7, // 初始饱和度 0.7
      0.45, // 初始亮度 0.45
  )
}
  • 首先依旧是生成随机的三维向量,调用
    randomColor()
    方法,用于后面计算HSL颜色,第一个分量的取值范围是
    [0, 0.5)
    ,与色相Hue的计算有关,第二个分量0.7,与饱和度的生成有关,第三个分量0.45,与亮度的生成有关
  • 然后在每一组圆上,依次设置每个圆的饱和度为0.4、0.55、0.7、0.85和1.0,设置每个圆的亮度为0.21、0.33、0.45、0.57和0.69

以上代码中,我们主要生成了一个随机的值,用于表示色相,通过
i * 0.25
加上随机值,来将每一行色相的角度拉开,从而保证三组圆之间的色相差异;并且每一组圆之间通过不同的饱和度和亮度做出区分。

从效果上看,比生成的RGB随机颜色要好不少。但是多试几次,还是能发现,有些颜色差距还是没那么明显。这是因为受到人眼视觉感知的影响。

我们可以通过一个简单的实验来直观感受这种影响:

for (let i = 0; i < 20; i ++) {
  ctx.fillStyle = `hsl(${Math.floor(i * 15)}, 50%, 50%)`;
  ctx.beginPath();
  ctx.arc((i - 10) * 20, 200, 10, 0, Math.PI * 2);
  ctx.fill();
}
for (let i = 0; i < 20; i ++) {
  ctx.fillStyle = `hsl(${Math.floor((i % 2 ? 60 : 210) + 3 * i)}, 50%, 50%`;
  ctx.beginPath();
  ctx.arc((i - 10) * 20, 140, 10, 0, Math.PI * 2);
  ctx.fill();
}

以上代码绘制了两排圆,第一排每个圆之间的色相间隔都是15,饱和度和亮度都是50%;第二排圆的颜色,色相在60和210附近两两交互,饱和度和亮度也都是50%。

观察第一排圆可以明显发现,虽然相邻的圆之间色相相差都是15,但颜色过渡并不均匀,尤其几个绿色的圆视觉上颜色比较接近;而第二排圆,虽然饱和度和亮度都是一样的,但蓝色和紫色的圆明显不如绿色和黄色的圆亮眼。这是由于人眼对不同频率的光的敏感度不同所产生的结果。也就是说,虽然
区分度够了,但是对于人眼感知HSL还是欠缺完美

因此我们还需要一套更接近人类知觉的颜色标准,它需要尽量满足2个原则:

第一,人眼看到的色差 = 颜色向量间的欧式距离,这样子计算出的颜色差值更能符合人眼视觉感知到的色差;

第二,相同亮度的两个颜色,能让人从视觉上也感觉亮度相同。

于是就诞生了CIE Lab。

CIE Lab和CIE Lch

CIE Lab颜色空间,简称Lab,是一种符合人类感觉的色彩空间,其中L表示亮度,a和b表示颜色对立度。

RGB色值也可以与Lab转换,但转换规则比较复杂。

比较欠缺的一点就是,目前还没有图形系统支持CIE Lab,但是css-color-level4规范已经给出了Lab颜色值的定义。

lab() = lab( [<percentage> | <number> | none]
      [ <percentage> | <number> | none]
      [ <percentage> | <number> | none]
      [ / [<alpha-value> | none] ]? )

尽管如此,一些走在前沿的探索者们已经开发出了可以直接处理Lab颜色空间的JavaScript库,比如d3-color。

以下的例子展示了d3.lab是如何处理Lab颜色的:

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.scale(1, -1); // 绕X轴翻转

for (let i = 0; i < 20; i ++) {
  const c = d3.lab(30, i * 15 - 150, i * 15 - 150).rgb();
  ctx.fillStyle = `rgb(${c.r}, ${c.g}, ${c.b})`;
  ctx.beginPath();
  ctx.arc((i - 10) * 20, 60, 10, 0, Math.PI * 2);
  ctx.fill();
}

for (let i = 0; i < 20; i ++) {
  const c = d3.lab(i * 5, 80, 80).rgb();
  ctx.fillStyle = `rgb(${c.r}, ${c.g}, ${c.b})`;
  ctx.beginPath();
  ctx.arc((i - 10) * 20, -60, 10, 0, Math.PI * 2);
  ctx.fill();
}

上述代码中使用d3.lab来定义Lab色值。

第一排圆,相邻的色值,欧式空间距离相同;第二排圆,颜色的亮度按5阶的方式递增。

在这里d3.lab处理Lab颜色的方式,就是把Lab色值转换为rgb色值后,再提供给Canvas2D使用。

看得出来,与HSL对比,使用Lab生成的颜色,更接近人眼的感知。

而CIE Lch和CIE Lab的关系,也是类似于将坐标从立方体的直角坐标系变换为圆柱体的极坐标系。

目前CIE Lch和CIE Lab的颜色表示方式还比较新,应用的也不太多,但由于符合人眼感知,可以对其保持关注。

Cubehelix色盘

最后一块是Cubehelix色盘,它的原理是,在RGB的立方体中,构建一段螺旋线,让色相随着亮度增加螺旋变换。就如下图所示:

可以看出,非常适合用于实现颜色随数据动态改变的效果。比如下面这个例子:

import {cubehelix} from 'cubehelix';
// ...
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.scale(1, -1); // 绕X轴翻转

const color = cubehelix(); // 色盘颜色映射函数
const T = 2000;

function update(t) {
  const p = 0.5 + 0.5 * Math.sin(t / T);
  console.log(p);
  ctx.clearRect(-256, -256, 512, 512);
  const {r, g, b} = color(p);
  ctx.fillStyle = `rgb(${255 * r}, ${255 * g}, ${255 * b})`;
  ctx.beginPath();
  ctx.rect(-236, -20, 480 * p, 40);
  ctx.fill();
  requestAnimationFrame(update);
}

update(0);

实现的效果如下:

可以看到颜色会随着时间的推延发生周期性的变化。

  • color是一个色盘映射函数,接收一个参数,参数值的范围为0到1。
  • 这里用正弦函数来模拟数据的周期性变化

总结

在前端开发中,颜色的使用随处可见,一般在开发过程中,有两种定义色值的方式。

第一种,是由UI设计师来指定全部配色,这也是普通前端开发中大多数的方式;

第二种,是根据数据来动态地生成颜色值,这在数据比较复杂的项目中比较常用。

对于第二种情况,颜色能在数据可视化中提供比较重要的信息,是值得我们重视的,而对于普通的前端开发,更好地掌握颜色的使用,也能为用户提供更加友好的交互。

背景

最近某集群扩容了一批物理机,其中 TiKV 节点有6台机器12个实例,同时调整了 label 设置增加了一层机柜级容灾。因为前期做了比较充分的准备工作,到了变更窗口只等着执行
scale-out
就行,操作过程也很顺利,很快就把所有节点都扩进去了,检查完各实例的运行状态,确保region已经开始正常调度,就放心去睡觉了(半夜变更,结束时凌晨1点左右)。

第二天一大早还在上班路上,业务方反馈数据库有部分SQL报错
Region is Unavailable
,怀疑新扩容的 TiKV 节点出了问题,火速赶到公司开始排查。



此时内心os,打工人1024不加班的小小心愿要破灭了。。

keycloak~从login-status-iframe相关文章,可阅读我的这两篇
keycloak~从login-status-iframe页面总结如何跨域传值
,
keycloak~对接login-status-iframe页面判断用户状态变更

什么是跨域

跨域(Cross-Origin)是指在Web开发中,当一个资源(比如JavaScript、CSS、图片等)来自于不同域名、协议或端口的网站时,就会出现跨域情况。浏览器的同源策略(Same-Origin Policy)限制了不同源之间的交互,包括JavaScript的跨域访问、Cookie的跨域发送等。

具体来说,如果一个页面的来源与另一个资源的来源在域名、协议、端口号中有任何一个不同,就被认为是跨域请求。例如,从a.com的页面向b.com发送AJAX请求、通过iframe嵌入不同域名的页面、在JavaScript中尝试读取不同域名的Cookie等都属于跨域操作。

跨域问题是由浏览器的安全策略引起的,它的存在是为了防止恶意网站利用用户的浏览器对其他网站进行攻击。在实际开发中,为了解决跨域问题,我们可以使用CORS(跨域资源共享)、JSONP(JSON with Padding)、代理服务器、iframe消息传递等技术来实现不同源之间的数据交互。

主域的判断

  • 顶级域名相同,二级域名不同,主域视为相同,如a.user.com,b.user.com,它们主域是user.com
  • 顶级域名相同,但端口不同,也是主域相同,如a.user.com,a.user.com:8080,它们主域是user.com
  • 顶级域名相同,二级域名不同,端口也不同,也是主域相同,如a.user.com:8080,b.user.com:8081,它们主域是user.com

跨域判断

  • 主域不同是跨域:a.user.com,b.product.com
  • 主域相同,子域不同,也是跨域:a.user.com,b.user.com
  • 域名相同,端口不同,也是跨域:a.user.com:8081,a.user.com:8081

跨域中的iframe的cookie值

实例代码

  • b.user.com
<script>
alert(document.cookie);
</script>
  • a.user.com和a.shop.com
<iframe src="https://b.user.com" id="my-iframe" style="display:none"></iframe>

代码在不同场景下测试结论

  • 主域名相同user.com
    • 运行a.user.com,可以通过alert输出b.user.com下的cookie内容
    • 运行a.user.com:8081,可以通过alert输出b.user.com下的cookie内容
  • 主域名不同shop.com和user.com
    • 运行a.shop.com,在打印的alert中,b.user.com的cookie是null

主域名相同

主域名不同

Partial Update

数据打宽

通过不同的流写不同的字段,打宽了数据的维度,填充了数据内容;如下所示:

--FlinkSQL参数设置
set
    `table.dynamic-table-options.enabled` = `true`;

SET
    `env.state.backend` = `rocksdb`;

SET
    `execution.checkpointing.interval` = `60000`;

SET
    `execution.checkpointing.tolerable-failed-checkpoints` = `3`;

SET
    `execution.checkpointing.min-pause` = `60000`;

--创建Paimon catalog
CREATE CATALOG paimon WITH (
    'type' = 'paimon',
    'metastore' = 'hive',
    'uri' = 'thrift://localhost:9083',
    'warehouse' = 'hdfs://paimon',
    'table.type' = 'EXTERNAL'
);

--创建Partial update结果表
CREATE TABLE if not EXISTS paimon.dw.order_detail (
    `order_id` string,
    `product_type` string,
    `plat_name` string,
    `ref_id` bigint,
    `start_city_name` string,
    `end_city_name` string,
    `create_time` timestamp(3),
    `update_time` timestamp(3),
    `dispatch_time` timestamp(3),
    `decision_time` timestamp(3),
    `finish_time` timestamp(3),
    `order_status` int,
    `binlog_time` bigint,
    PRIMARY KEY (order_id) NOT ENFORCED
) WITH (
    'bucket' = '20',
    -- 指定20个bucket
    'bucket-key' = 'order_id',
    -- 记录排序字段
    'sequence.field' = 'binlog_time',
    -- 选择 full-compaction ,在compaction后产生完整的changelog
    'changelog-producer' = 'full-compaction',
    -- compaction 间隔时间
    'changelog-producer.compaction-interval' = '2 min',
    'merge-engine' = 'partial-update',
    -- 忽略DELETE数据,避免运行报错
    'partial-update.ignore-delete' = 'true'
);

INSERT INTO
    paimon.dw.order_detail 
-- order_info表提供主要字段
SELECT
    order_id,
    product_type,
    plat_name,
    ref_id,
    cast(null as string) as start_city_name,
    cast(null as string) as end_city_name,
    create_time,
    update_time,
    dispatch_time,
    decision_time,
    finish_time,
    order_status,
    binlog_time
FROM
    paimon.ods.order_info
    /*+ OPTIONS ('scan.mode'='latest') */
union
all 
-- order_address表提供城市字段
SELECT
    order_id,
    cast(null as string) as product_type,
    cast(null as string) as plat_name,
    cast(null as bigint) as ref_id,
    start_city_name,
    end_city_name,
    cast(null as timestamp(3)) as create_time,
    cast(null as timestamp(3)) as update_time,
    cast(null as timestamp(3)) as dispatch_time,
    cast(null as timestamp(3)) as decision_time,
    cast(null as timestamp(3)) as finish_time,
    cast(null as int) as order_status,
    binlog_time
FROM
    paimon.ods.order_address
    /*+ OPTIONS ('scan.mode'='latest') */
;

完整的Changlog

Paimon中的表被多流填充数据且打宽维度后,支持流读、批读的方式提供完整的Changelog给下游。

Sequence-Group

配置:
'fields.G.sequence-group'='A,B'
由字段
G
控制是否更新字段
A, B
;总得来说,
G
的值如果为null或比更新值大将不更新
A,B
;如下单测

public void testSequenceGroup() {
    sql(
            "CREATE TABLE SG ("
                    + "k INT, a INT, b INT, g_1 INT, c INT, d INT, g_2 INT, PRIMARY KEY (k) NOT ENFORCED)"
                    + " WITH ("
                    + "'merge-engine'='partial-update', "
                    + "'fields.g_1.sequence-group'='a,b', "
                    + "'fields.g_2.sequence-group'='c,d');");

    sql("INSERT INTO SG VALUES (1, 1, 1, 1, 1, 1, 1)");

    // g_2 should not be updated
    sql("INSERT INTO SG VALUES (1, 2, 2, 2, 2, 2, CAST(NULL AS INT))");

    // select *
    assertThat(sql("SELECT * FROM SG")).containsExactlyInAnyOrder(Row.of(1, 2, 2, 2, 1, 1, 1));

    // projection
    assertThat(sql("SELECT c, d FROM SG")).containsExactlyInAnyOrder(Row.of(1, 1));

    // g_1 should not be updated
    sql("INSERT INTO SG VALUES (1, 3, 3, 1, 3, 3, 3)");

    assertThat(sql("SELECT * FROM SG")).containsExactlyInAnyOrder(Row.of(1, 2, 2, 2, 3, 3, 3));

    // d should be updated by null
    sql("INSERT INTO SG VALUES (1, 3, 3, 3, 2, 2, CAST(NULL AS INT))");
    sql("INSERT INTO SG VALUES (1, 4, 4, 4, 2, 2, CAST(NULL AS INT))");
    sql("INSERT INTO SG VALUES (1, 5, 5, 3, 5, CAST(NULL AS INT), 4)");

    assertThat(sql("SELECT a, b FROM SG")).containsExactlyInAnyOrder(Row.of(4, 4));
    assertThat(sql("SELECT c, d FROM SG")).containsExactlyInAnyOrder(Row.of(5, null));
}

其作用是:

  1. 在多个数据流更新期间的无序问题。每个数据流都定义自己的序列组。
  2. 真正的部分更新,而不仅仅是非空值的更新。
  3. 接受删除记录来撤销部分列。

Changelog-Producer

Paimon通过Changelog-Producer支持生成changelog,并支持下游以流读、批读的形式读取changelog。
Changelog的生成有多种方式,input、lookup、full-compaction;其生成代价是由低到高。

None

不查找旧值,不额外写Changelog;但会下游任务中通过ChangelogNormalize算子补足Changelog。

Input

不查找旧值,额外写Changelog;适用与CDC的数据源。

Lookup

查找旧值,额外写Changelog;如果不是CDC数据源,需要通过LookupCompaction查找旧值,即在 compaction 的过程中, 会去向高层查找本次新增 key 的旧值, 如果没有查找到, 那么本次的就是新增 key, 如果有查找到, 那么就生成完整的 UB 和 UA 消息。

Full-Compaction

查找旧值,额外写Changelog;在 full compact 的过程中, 其实数据都会被写到最高层, 所以所有 value 的变化都是可以推演出来的.

数据一致性

数据版本

通过Flink的checkpoint机制,生成Snapshot并标记版本,即,一个Snapshot对应数据的一个版本。
比如 Job-A 基于 Table-A 的 Snapshot-20 产出了 Table-B 的 Snapshot-11。Job-B 基于 Table-A 的Snapshot-20产出了 Table-C 的 Snapshot-15。那么 Job-C 的查询就应该基于 Table-B 的 Snapshot-11 和 Table-C 的 Snapshot-15 进行计算,明确了数据版本,从而实现计算的一致性。
image.png

生成的snapshot-xx,就是数据的版本号。

数据对齐

将 Checkpoint 插入到两个 Snapshot 的数据之间。如果当前的 Snapshot 还没有完全被消费,这个 Checkpoint 的触发会被推迟,从而实现按照 Snapshot 对数据进行划分和对齐。
image.png
实现分为两个部分。

  • 在提交阶段,需要去血缘关系表中查询上下游表的一致性版本,并且基于查询结果给对应的上游表设置起始的消费位置。
  • 在运行阶段,按照消费的 Snapshot 来协调 Checkpoint,在 Flink 的 Checkpoint Coordinator 向 Source 发出 Checkpoint 的请求时,会强制要求将 Checkpoint 插入到两个 Snapshot 的数据之间。如果当前的 Snapshot 还没有完全被消费,这个 Checkpoint 的触发会被推迟,从而实现按照 Snapshot 对数据进行划分和处理。

数据血缘

概念

数据从产生到消费的整个流转过程中所经历的各种转换、处理和流动的轨迹。数据血缘提供了数据的来源、去向以及中间处理过程的透明度,帮助用户理解数据如何在系统中被处理和移动,以及数据是如何从原始状态转化为最终的可消费形态。

实现

在checkpoint的提交时将数据的血缘关系写入到System Table,记录血缘关系。
image.png