首页> 搜索结果页
"跨进程api hook" 检索
共 58 条结果
《Android安全技术揭秘与防范》—第8章8.节什么是Hook技术
本节书摘来自异步社区《Android安全技术揭秘与防范》一书中的第8章8.节什么是Hook技术,作者周圣韬,更多章节内容可以访问云栖社区“异步社区”公众号查看。 第8章 动态注入技术Android安全技术揭秘与防范我们在讨论动态注入技术的时候,APIHook的技术由来已久,在操作系统未能提供所需功能的情况下,利用APIHook的手段来实现某种必需的功能也算是一种不得已的办法。在Windows平台下开发电子词典的光标取词功能,这项功能就是利用Hook API的技术把系统的字符串输出函数替换成了电子词典中的函数,从而能得到屏幕上任何位置的字符串。无论是16位的Windows95,还是32位的Windws NT,都有办法向整个系统或特定的目标进程中“注入”DLL动态库,并替换掉其中的函数。 但是在Android上进行Hook需要跨进程操作,我们知道在Linux上的跨进程操作需要Root权限。所以目前Hook技术广泛地应用在安全类软件的主动防御上,所见到的Hook类病毒并不多。 Android系统在开发中会存在两种模式,一个是Linux的Native模式,而另一个则是建立在虚拟机上的Java模式。所以,我们在讨论Hook的时候,可想而知在Android平台上的Hook分为两种。一种是Java层级的Hook,另一种则是Native层级的Hook。两种模式下,我们通常能够通过使用JNI机制来进行调用。但我们知道,在Java中我们能够使用native关键字对C/C++代码进行调用,但是在C/C++中却很难调用Java中的代码。所以,我们能够在Java层级完成的事基本也不会在Native层去完成。 8.1 什么是Hook技术还没有接触过Hook技术读者一定会对Hook一词感觉到特别的陌生,Hook英文翻译过来就是“钩子”的意思,那我们在什么时候使用这个“钩子”呢?我们知道,在Android操作系统中系统维护着自己的一套事件分发机制。应用程序,包括应用触发事件和后台逻辑处理,也是根据事件流程一步步地向下执行。而“钩子”的意思,就是在事件传送到终点前截获并监控事件的传输,像个钩子钩上事件一样,并且能够在钩上事件时,处理一些自己特定的事件。较为形象的流程如图8-1所示。 Hook的这个本领,使它能够将自身的代码“融入”被勾住(Hook)的程序的进程中,成为目标进程的一个部分。我们也知道,在Android系统中使用了沙箱机制,普通用户程序的进程空间都是独立的,程序的运行彼此间都不受干扰。这就使我们希望通过一个程序改变其他程序的某些行为的想法不能直接实现,但是Hook的出现给我们开拓了解决此类问题的道路。当然,根据Hook对象与Hook后处理的事件方式不同,Hook还分为不同的种类,如消息Hook、API Hook等。 8.1.1 Hook原理Hook技术无论对安全软件还是恶意软件都是十分关键的一项技术,其本质就是劫持函数调用。但是由于处于Linux用户态,每个进程都有自己独立的进程空间,所以必须先注入到所要Hook的进程空间,修改其内存中的进程代码,替换其过程表的符号地址。在Android中一般是通过ptrace函数附加进程,然后向远程进程注入so库,从而达到监控以及远程进程关键函数挂钩。 Hook技术的难点,并不在于Hook技术,初学者借助于资料“照葫芦画瓢”能够很容易就掌握Hook的基本使用方法。如何找到函数的入口点、替换函数,这就涉及了理解函数的连接与加载机制。 从Android的开发来说,Android系统本身就提供给了我们两种开发模式,基于Android SDK的Java语言开发,基于AndroidNDK的Native C/C++语言开发。所以,我们在讨论Hook的时候就必须在两个层面上来讨论。对于Native层来说Hook的难点其实是在理解ELF文件与学习ELF文件上,特别是对ELF文件不太了解的读者来说;对于Java层来说,Hook就需要了解虚拟机的特性与Java上反射的使用。 8.1.1.1 Hook工作流程之前我们介绍过Hook的原理就是改变目标函数的指向,原理看起来并不复杂,但是实现起来却不是那么的简单。这里我们将问题细分为两个,一个是如何注入代码,另一个是如何注入动态链接库。 注入代码我们就需要解决两个问题。 需要注入的代码我们存放在哪里?如何注入代码?注入动态共享库我们也需要解决两个问题: 我们不能只在自己的进程载入动态链接库,如何使进程附着上目标进程?如何让目标进程调用我们的动态链接库函数?这里我也不卖关子了,说一下目前对上述问题的解决方案吧。对于进程附着,Android的内核中有一个函数叫ptrace,它能够动态地attach(跟踪一个目标进程)、detach(结束跟踪一个目标进程)、peektext(获取内存字节)、poketext(向内存写入地址)等,它能够满足我们的需求。而Android中的另一个内核函数dlopen,能够以指定模式打开指定的动态链接库文件。对于程序的指向流程,我们可以调用ptrace让PC指向LR堆栈。最后调用,对目标进程调用dlopen则能够将我们希望注入的动态库注入至目标进程中。 对于代码的注入(Hook API),我们可以使用mmap函数分配一段临时的内存来完成代码的存放。对于目标进程中的mmap函数地址的寻找与Hook API函数地址的寻找都需要通过目标进程的虚拟地址空间解析与ELF文件解析来完成,具体算法如下。 通过读取 /proc//maps文件找到链接库的基地址。读取动态库,解析ELF文件,找到符号(需要对ELF文件格式的深入理解)。计算目标函数的绝对地址。目标进程函数绝对地址= 函数地址 + 动态库基地址 上面说了这么多,向目标进程中注入代码总结后的步骤分为以下几步。 (1)用ptrace函数attach上目标进程。 (2)发现装载共享库so函数。 (3)装载指定的.so。 (4)让目标进程的执行流程跳转到注入的代码执行。 (5)使用ptrace函数的detach释放目标进程。 对应的工作原理流程如图8-2所示。 https://yqfile.alicdn.com/b7f12bd9b13e8202d3e5547157ba0750ef5c268d.png" > 8.1.1.2 ptrace函数说到了Hook我们就不能不说一下ptrace函数,ptrace提供了一种使父进程得以监视和控制其他进程的方式,它还能够改变子进程中的寄存器和内核映像,因而可以实现断点调试和系统调用的跟踪。使用ptrace,你可以在用户层拦截和修改系统调用(这个和Hook所要达到的目的类似),父进程还可以使子进程继续执行,并选择是否忽略引起终止的信号。 ptrace函数定义如下所示: int ptrace(int request, int pid, int addr, int data);request是请求ptrace执行的操作。pid是目标进程的ID。addr是目标进程的地址值。data是作用的数据。对于ptrace来说,它的第一个参数决定ptrace会执行什么操作。常用的有跟踪指定的进程(PTRACE_ATTACH)、结束跟踪指定进程(PTRACE_DETACH)等。详细的参数与使用方式如表8-1所示。 8.1.2 Hook的种类我们所讨论的Hook,也就是平时我们所说的函数挂钩、函数注入、函数劫持等操作。针对Android操作系统,根据API Hook对应的API不一样我们可以分为使用Android SDK开发环境的Java API Hook与使用Android NDK开发环境的Native API Hook。而对于Android中so库文件的函数Hook,根据ELF文件的特性能分为Got表Hook、Sym表Hook以及inline Hook等。当然,根据Hook方式的应用范围我们在Android这样一个特殊的环境中还能分别出全局Hook与单个应用程序Hook。本节,我们就具体地说说这些Hook的原理以及这些Hook方式给我们使用Hook带来的便利性。 TIPS 对于Hook程序的运行环境不同,还可以分为用户级API Hook与内核级API Hook。用户级API Hook主要是针对在操作系统上为用户所提供的API函数方法进行重定向修改。而内核级API Hook则是针对Android内核Linux系统提供的内核驱动模式造成的函数重定向,多数是应用在Rootkit中。8.1.2.1 Java层API Hook通过对Android平台的虚拟机注入与Java反射的方式,来改变Android虚拟机调用函数的方式(ClassLoader),从而达到Java函数重定向的目的。这里我们将此类操作称为Java API Hook。因为是根据Java中的发射机制来重定向函数的,那么很多Java中反射出现的问题也会在此出现,如无法反射调用关键字为native的方法函数(JNI实现的函数),基本类型的静态常量无法反射修改等。 8.1.2.2 Native层So库Hook主要是针对使用NDK开发出来的so库文件的函数重定向,其中也包括对Android操作系统底层的Linux函数重定向,如使用so库文件(ELF格式文件)中的全局偏移表GOT表或符号表SYM表进行修改从而达到的函数重定向,我们有可以对其称为GOT Hook和SYM Hook。针对其中的inline函数(内联函数)的Hook称为inline Hook。 8.1.2.3 全局Hook针对Hook的不同进程来说又可以分为全局Hook与单个应用程序进程Hook,我们知道在Android系统中,应用程序进程都是由Zygote进程孵化出来的,而Zygote进程是由Init进程启动的。Zygote进程在启动时会创建一个Dalvik虚拟机实例,每当它孵化一个新的应用程序进程时,都会将这个Dalvik虚拟机实例复制到新的应用程序进程里面去,从而使每一个应用程序进程都有一个独立的Dalvik虚拟机实例。所以如果选择对Zygote进程Hook,则能够达到针对系统上所有的应用程序进程Hook,即一个全局Hook。对比效果如图8-3所示。 而对应的app_process正是zygote进程启动一个应用程序的入口,常见的Hook框架Xposed与Cydiasubstrate也是通过替换app_process来完成全局Hook的。 8.1.3 Hook的危害API Hook技术是一种用于改变API执行结果的技术,能够将系统的API函数执行重定向。一个应用程序调用的函数方法被第三方 Hook 重定向后,其程序执行流程与执行结果是无法确认的,更别提程序的安全性了。而Hook技术的出现并不是为病毒和恶意程序服务的,Hook技术更多的是应用在安全管理软件上面。但是无论怎么说,已经被Hook后的应用程序,就毫无安全可言了。
文章
安全  ·  Java  ·  Linux  ·  API  ·  Android开发
2017-05-02
AssetHook:Android应用资源数据运行时编辑工具
本文讲的是AssetHook:Android应用资源数据运行时编辑工具,AssetHook是一个工具,它可以让Android安全研究人员和普通用户能够在无需修改APK本身的情况下随时修改Android应用程序的部分Asset。这样的修改使研究人员可以改变嵌入式数据,以更好地评估和测试移动应用程序。目前来看AssetHook比现有方法更容易使用,且比传统方法更有效。 背景 去年年底,我开始关注Android启用React Native 后 Facebook的新框架,它将跨平台移动开发统一到JavaScript,显然JavaScript是一种不需要介绍的语言。在React Native之前,在JavaScript中构建跨平台移动应用程序的主要方法是使用PhoneGap(…或者现在是Cordova吗?甚至不让我从Ionic开始)。这些都是基于将JavaScript加载到Webview中(UIWebView在iOS上,WebView在Android上)的主要应用程序逻辑。然后,开发人员可以在平台的“本机”语言中编写一些平台特定的代码,并将它们与幽灵一般的webview魔术FFI连接在一起。 React Native将这种设计(也可能是理由)一脚踢开,然后颠覆了传统。它不是使用webview和HTML / CSS进行渲染,它将JavaScript声明的UI映射到平台的本地UI工具包,并嵌入WebKit的JavaScriptCore(JSC)库来运行该JavaScript,完全避免了平台的Webview实现。JSC支持在没有JIT的情况下解释JavaScript,这在iOS上是必需的,因为它的安全模型是基于不允许任何人生成可执行内存的(而且人们说OpenBSD的人是Luddites for W ^ X …); 最后我检查过,V8只是JIT(新的ignition interpreter似乎并没有改变,因为“ JIT代码生成仍然是IC和代码存根 ”)。 作为我最初在Android上进行React Native的一部分,我试图在应用程序中注入额外的JavaScript代码,以将开发控制台REPL加载(并执行janky函数hook)。所以,我在这种情况下做了一个通常的工作,并为Android的android.content.res.AssetManager类写了一个Xposed函数hook。..之后我再没有做什么。 Android使用AssetManager该类作为应用程序的界面来访问嵌入在其APK中的特定类型的文件资源(特别是APK中的assets/目录中)。鉴于我知道(从开放一个发布版本的React Native应用程序)主要的后处理JavaScript软件包作为资产存储,所以hook不起作用是非常奇怪的。然后,我通过React Native的Android代码库开始测试,很快就发现使用C API为Android的资产管理器加载了C ++代码,这解释了为什么Java函数挂起不起作用。 因为我想要修改这些软件包的内容,而不需要创建和签名修改的APK(然后需要更多的hook来欺骗他们的签名),我设置了构建一个工具来hook这些资源负载。我在C ++中写了最初的POC,然后在Rust中重写。 (A)AssetHook AssetHook是一款LD_PRELOAD用于Android应用程序的函数hook库。它hook了Android的内部资产管理代码,并将资产文件加载从合法(即签名的)APK重定向到设备本地文件系统上的单独位置。(A)AssetHook的初始版本“AAssetHook”(两个“A”)拦截了Android 公共 AAssetManager C API的调用,其实际上是用extern "C"C ++编写的。然而,由于这个API只是一个直接由Java 类使用的Android 内部类的封装,所以我把它重写为“AssetHook”(一个'A')来代替底层的C ++ API。 AssetManagerandroid.content.res.AssetManager 如果没有像AssetHook这样的东西的话,替换Android资产将需要解压APK,替换资产文件,重新打包APK,可能会重新对齐,签署新的APK文件,然后安装它。这不但是一个繁琐和缓慢的过程(特别是APK复制到设备并为更大的应用程序安装过程),还必须处理新的签名打破跨应用程序签名检查的后果。对于使用自定义权限,共享用户ID或自定义IPC访问控制的应用程序组,此新签名将阻止修改的应用程序与其他应用程序进行交互或完全不起作用。然后需要额外的hook来欺骗修改后的APK的签名证书以匹配原来的APK。此外,如果没有更多根深蒂固和灵活的功能hook基础架构(例如修改系统分区二进制文件的Xposed),这种hook在Android上可能是不可能的。使用这种框架的一个关键问题是由于需要移植工作,因此必然会延迟支持新的Android版本。 C API Hooking 使用了 LD_PRELOAD C API hooking变体(“AAssetHook”)是由vanilla的 LD_PRELOADhook来实现的,它声明了一些重复的函数符号,尽管动态链接的魔力 – 在从其二进制文件外部访问时重写对这些函数的调用。然后在dlopen(3)/ dlsym(3)(在Unix上)使用一个Rust包装库来根据需要代理对原始函数的调用。之后,它会检查一个APK中的给定文件路径是否与设备文件系统上的现有路径匹配,并欺骗C API返回磁盘文件的文件内容,而不是in- C++ API Hooking 使用了 LD_PRELOAD 由于资产管理器的C ++实现不是公共API,C ++ APIhook变体(“AssetHook”)稍微复杂一点,这在很大程度上依赖于多态性,并且在较小的Android版本中可能会发生变化。当在C ++中使用多态时,通常将通过动态调度处理虚拟方法调用。在大多数“平稳”平台上,通过将虚拟表(vtable)指针作为类的内存结构的第一个元素之一来实现。该指针在构造期间设置,并指向一个填充有该类使用的特定虚拟方法的函数指针的表。LD_PRELOAD在这种情况下,函数hook非常有限,因为它只能挂接导出符号的二进制函数调用,而动态调度调用则使用直接函数指针。这样可以避免基于vtable的呼叫被LD_PRELOAD符号覆盖直接挂起。此外,这些函数指针的顺序很可能会在次级类定义更改时发生更改。在不同的编译器之间也是不一致的,通常可能难以推断出多个版本的二进制文件。 注意:这完全取决于编译器,但是这一点在Unix上的Clang和GCC(至少对于x86 / amd64和ARM / AArch64来说都是一致的)。 例如,下面的代码片段的对象可以被布置,如下图所示: #include <stdio.h>struct Base {   virtual void foo() {     puts("base!");   }   size_t a = (size_t)-};struct Derived : public Base {   virtual void foo() {     puts("derived!");   }   size_t b = (size_t)-};struct DDerived : public Derived {   void foo() final override {     puts("dderived!");   }   size_t c = (size_t)-};void call_foo(Base& br.foo();}int main() {   Base b;   Derived d;   DDerived dd;   call_foo(b);   call_foo(d);   call_foo(dd);   return 0;}   功能搜索和vtable插槽映射 处理vtable排序的波动的一种方法是将给定的类“vtable”与其二进制文件的符号表进行交叉引用(用binutils的GPL bfd.h或libelfin解析)。使用手中的函数地址,可以对vtable进行交叉引用以找到其偏移量。然而,这可能不起作用,因为一些虚拟方法函数可能没有符号,就像Android资产管理器的C ++实现一样。另一种方法是扫描这种不符号的函数的原始字节,但这不太可能跨越多个二进制版本。  事实上,这是完全可能的(使用类似Capstone的东西)去解析调用目标类的虚拟方法并提取这些方法的vtable偏移量的符号符号函数的汇编。 vtable Slot Knocking AssetHook的C ++ APIhook实现使用了最后一种方法的一个变体,依靠这样一个事实,即将虚拟方法调用的C API本身就是具有ABI稳定性保证的公共API。AssetHook不是分析C API函数的指令来剔除vtable偏移量,而是创建一个假的C ++对象,并使用它来配置vtable。该对象vtable指针指向一个虚构的vtable,其中包含一系列报告调用顺序的函数指针。AssetHook将该对象作为嵌入到void*掩码包装结构体中的指针传递给C API,然后C API通过编译器提供的偏移量调用关联的虚拟方法。执行此调用时,将触发嵌入式函数指针,暴露给定操作的vtable偏移量。这允许通过调用这个假的对象上已知的C API函数来逐个获取实际的vtable顺序,以获得它们包装的C ++方法的vtable偏移量。 注意:对于特殊的涉及 thunk的 “复杂”类,需要更加动态地分析vtable,因为类别的vtable段中的“正常”成员函数指针实际上是由编译器生成的包装函数。该函数相应地移动this指针,并从实际类的更大的vtable中的其他地方调用“真实”成员函数指针。 hook虚拟方法 之后,在返回对象指针LD_PRELOAD的AssetManager类的“公共”非虚拟方法的变形符号名称上创建一个类型的hookAsset。这个hook确定文件是否应该被hooking,如果是这样,返回一个指向一个修改后的Asset对象的指针,其中一个vtable包含了函数hook。 例子: 在这个例子中,我们将使用AssetHook将我们自己的JavaScript文件交换到Tic-tac-toe示例应用程序中(参考React Native文档,我必须传递–dev false给react-native build它来使其大部分应用程序成为最小化的)。我们假设您按照文件中的描述安装了它。 1、安装一个“release”版本的应用程序(为了尽可能的缩小,AssetHook可以同时发布和调试Android APK版本)在一个根深蒂固的测试设备上(AssetHook已知可以与Android 5.x,6.x和7.x一起用32位和64位ARM,以及32位x86上的Android O Developer Preview)。 2、从APK文件中提取assets/index.android.bundle(从嵌入的Android来源app/build目录或使用该设置adb shell pm list packages -f | grep <pkg>和adb pull)。 3、修改文件并注入JavaScript alert(…)。 4、运行以下命令: $ adb push path/to/index.android.bundle /data/local/tmp/assethook/<pkg>/assets/ $ adb shell su -c 'setprop wrap.<pkg> LD_PRELOAD=/data/local/tmp/lib/libassethook_cppapi.so' 注意: React Native使用32位二进制文件,甚至在64位Android上。因此,32位模式下进程加载,我们需要使用32位版本的AssetHook。 5、启动应用程序(如果已经打开,请先关闭它)。 未来的工作 主要优先事项是在执行模式下完全启用SEAndroid时支持Android。现在,AssetHook要求将SELinux置于允许模式,因为替换文件存在于Google试图通过SELinux限制访问现代Android版本的共享临时目录中。我正在寻求支持从应用程序自己的内部目录加载替换资产文件,但这可能需要额外的工具来将文件上传到设备上。 原文发布时间为:2017年6月3日 本文作者:Change 本文来自云栖社区合作伙伴嘶吼,了解相关信息可以关注嘶吼网站。 原文链接
文章
JavaScript  ·  前端开发  ·  API  ·  Android开发  ·  编译器  ·  iOS开发  ·  Rust  ·  调度  ·  Java  ·  安全
2017-09-18
Kubernetes-架构路线图
1、背景 各种平台都会遇到一个不可回避的问题,即平台应该包含什么和不包含什么,Kubernetes也一样。Kubernetes作为一个部署和管理容器的平台,Kubernetes不能也不应该试图解决用户的所有问题。Kubernetes必须提供一些基本功能,用户可以在这些基本功能的基础上运行容器化的应用程序或构建它们的扩展。Kubernetes必须提供一些基本功能,用户可以在运行他们的容器化应用程序或构建其扩展时依赖,但Kubernetes不能也不应该试图解决用户所拥有的每一个问题。此文件旨在明确Kubernetes架构的设计意图,打算描述其演进和将来的开发蓝图。NIY标记尚未实现的功能特性。 此文档用于描述Kubernetes系统的的架构开发演进过程,以及背后的驱动原因。对于希望扩展或者定制kubernetes系统的开发者,其应该使用此文档作为向导,以明确可以在那些地方,以及如何进行增强功能的实现。如果应用开发者需要开发一个大型的、便携和符合将来发展的kubernetes应用,也应该参考此文档,以了解Kubernetes将来的演化和发展。 从逻辑上来看,kubernetes的架构分为如下几个层次: 核心层(Nucleus): 提供标准的API和执行机,包括基本的REST机制,安全、Pod、容器、网络接口和存储卷管理,通过接口能够对这些API和执行机进行扩展,核心层是必需的,它是系统最核心的一部分。 应用管理层(Application Management Layer ):提供基本的部署和路由,包括自愈能力、弹性扩容、服务发现、负载均衡和流量路由。此层即为通常所说的服务编排,这些功能都提供了默认的实现,但是允许进行一致性的替换。 治理层(The Governance Layer):提供高层次的自动化和策略执行,包括单一和多租户、度量、智能扩容和供应、授权方案、网络方案、配额方案、存储策略表达和执行。这些都是可选的,也可以通过其它解决方案实现。 接口层(The Interface Layer):提供公共的类库、工具、用户界面和与Kubernetes API交互的系统。 生态层(The Ecosystem):包括与Kubernetes相关的所有内容,严格上来说这些并不是Kubernetes的组成部分。包括CI/CD、中间件、日志、监控、数据处理、PaaS、serverless/FaaS系统、工作流、容器运行时、镜像仓库、Node和云提供商管理等。 2、系统分层 就像Linux拥有内核(kernel)、核心系统类库、和可选的用户级工具,kubernetes也拥有功能和工具的层次。对于开发者来说,理解这些层次是非常重要的。kubernetes APIs、概念和功能都在下面的层级图中得到体现。 2.1 核心层:API和执行(The Nucleus: API and Execution) 核心层包含最核心的API和执行机。 这些API和功能由上游的kubernetes代码库实现,由最小特性集和概念集所组成,这些特征和概念是系统上层所必需的。 这些由上游KubNeNETs代码库实现的API和函数包括建立系统的高阶层所需的最小特征集和概念集。这些内容被明确的地指定和记录,并且每个容器化的应用都会使用它们。开发人员可以安全地假设它们是一直存在的,这些内容应该是稳定和乏味的。 2.1.1 API和集群控制面板 Kubernetes集群提供了类似REST API的集,通过Kubernetes API server对外进行暴露,支持持久化资源的增删改查操作。这些API作为控制面板的枢纽。 遵循Kubernetes API约定(路径约定、标准元数据等)的REST API能够自动从共享API服务(认证、授权、审计日志)中收益,通用客户端代码能够与它们进行交互。 作为系统的最娣层,需要支持必要的扩展机制,以支持高层添加功能。另外,需要支持单租户和多租户的应用场景。核心层也需要提供足够的弹性,以支持高层能扩展新的范围,而不需要在安全模式方面进行妥协。 如果没有下面这些基础的API机和语义,Kubernetes将不能够正常工作: 认证(Authentication): 认证机制是非常关键的一项工作,在Kubernetes中需要通过服务器和客户端双方的认证通过。API server 支持基本认证模式 (用户命名/密码) (注意,在将来会被放弃), X.509客户端证书模式,OpenID连接令牌模式,和不记名令牌模式。通过kubeconfig支持,客户端能够使用上述各种认证模式。第三方认证系统可以实现TokenReview API,并通过配置认证webhook来调用,通过非标准的认证机制可以限制可用客户端的数量。 The TokenReview API (与hook的机制一样) 能够启用外部认证检查,例如Kubelet Pod身份标识通过”service accounts“提供 The ServiceAccount API,包括通过控制器创建的默认ServiceAccount保密字段,并通过接入许可控制器进行注入。 授权(Authorization):第三方授权系统可以实现SubjectAccessReview API,并通过配置授权webhook进行调用。 SubjectAccessReview (与hook的机制一样), LocalSubjectAccessReview, 和SelfSubjectAccessReview APIs能启用外部的许可检查,诸如Kubelet和其它控制器。 REST 语义、监控、持久化和一致性保证、API版本控制、违约、验证 NIY:需要被解决的API缺陷: 混淆违约行为 缺少保障 编排支持 支持事件驱动的自动化 干净卸载 NIY: 内置的准入控制语义、同步准入控制钩子、异步资源初始化 — 发行商系统集成商,和集群管理员实现额外的策略和自动化 NIY:API注册和发行、包括API聚合、注册额外的API、发行支持的API、获得支持的操作、有效载荷和结果模式的详细信息。 NIY:ThirdPartyResource和ThirdPartyResourceData APIs (或她们的继承者),支持第三方存储和扩展API。 NIY:The Componentstatuses API的可扩展和高可用的替代,以确定集群是否完全体现和操作是否正确:ExternalServiceProvider (组件注册) The Endpoints API,组件增持需要,API服务器端点的自我发布,高可用和应用层目标发行 The Namespace API,用户资源的范围,命名空间生命周期(例如:大量删除) The Event API,用于对重大事件的发生进行报告,例如状态改变和错误,以及事件垃圾收集 NIY:级联删除垃圾收集器、finalization, 和orphaning NIY: 需要内置的add-on的管理器 ,从而能够自动添加自宿主的组件和动态配置到集群,在运行的集群中提取出功能。 Add-ons应该是一个集群服务,作为集群的一部分进行管理 它们可以运行在kube-system命名空间,这么就不会与用户的命名进行冲突 API server作为集群的网关。根据定义,API server必需能够被集群外的客户端访问,而Node和Pod是不被集群外的客户端访问的。客户端认证API server,并使用API server作为堡垒和代理/通道来通过/proxy和/portforward API访问Node和Pod等Clients authenticate the API server and also use it TBD:The CertificateSigningRequest API,能够启用认证创建,特别是kubele证书。 理想情况下,核心层API server江仅仅支持最小的必需的API,额外的功能通过聚合、钩子、初始化器、和其它扩展机制来提供。注意,中心化异步控制器以名为Controller Manager的独立进程运行,例如垃圾收集。 API server依赖下面的外部组件: 持久化状态存储 (etcd,或相对应的其它系统;可能会存在多个实例) API server可以依赖: 身份认证提供者 The TokenReview API实现者 实现者 The SubjectAccessReview API实现者 2.1.2 执行 在Kubernetes中最重要的控制器是kubelet,它是Pod和Node API的主要实现者,没有这些API的话,Kubernetes将仅仅只是由键值对存储(后续,API机最终可能会被作为一个独立的项目)支持的一个增删改查的REST应用框架。 Kubernetes默认执行独立的应用容器和本地模式。 Kubernetes提供管理多个容器和存储卷的Pod,Pod在Kubernetes中作为最基本的执行单元。 Kubelet API语义包括: The Pod API,Kubernetes执行单元,包括: Pod可行性准入控制基于Pod API中的策略(资源请求、Node选择器、node/pod affinity and anti-affinity, taints and tolerations)。API准入控制可以拒绝Pod或添加额外的调度约束,但Kubelet才是决定Pod最终被运行在哪个Node上的决定者,而不是schedulers or DaemonSets。 容器和存储卷语义和生命周期 Pod IP地址分配(每个Pod要求一个可路由的IP地址) 将Pod连接至一个特定安全范围的机制(i.e., ServiceAccount) 存储卷来源: emptyDir hostPath secret configMap downwardAPI NIY:容器和镜像存储卷 (and deprecate gitRepo) NIY:本地存储,对于开发和生产应用清单不需要复杂的模板或独立配置 flexVolume (应该替换内置的cloud-provider-specific存储卷) 子资源:绑定、状态、执行、日志、attach、端口转发、代理 NIY:可用性和引导API 资源检查点 容器镜像和日志生命周期 The Secret API,启用第三方加密管理 The ConfigMap API,用于组件配置和Pod引用 The Node API,Pod的宿主 在一些配置中,可以仅仅对集群管理员可见 Node和pod网络,业绩它们的控制(路由控制器) Node库存、健康、和可达性(node控制器) Cloud-provider-specific node库存功能应该被分成特定提供者的控制器 pod终止垃圾收集 存储卷控制器 Cloud-provider-specific attach/detach逻辑应该被分成特定提供者的控制器,需要一种方式从API中提取特定提供者的存储卷来源。 The PersistentVolume API NIY:至少被本地存储所支持 The PersistentVolumeClaim API 中心化异步功能,诸如由Controller Manager执行的pod终止垃圾收集。 当前,控制过滤器和kubelet调用“云提供商”接口来询问来自于基础设施层的信息,并管理基础设施资源。然而,kubernetes正在努力将这些触摸点(问题)提取到外部组件中,不可满足的应用程序/容器/OS级请求(例如,PODS,PersistentVolumeClaims)作为外部“动态供应”系统的信号,这将使基础设施能够满足这些请求,并使用基础设施资源(例如,Node、和PersistentVolumes)在Kubernetes进行表示,这样Kubernetes可以将请求和基础设施资源绑定在一起。 对于kubelet,它依赖下面的可扩展组件: 镜像注册 容器运行时接口实现 容器网络接口实现 FlexVolume 实现(”CVI” in the diagram) 以及可能依赖: NIY:第三方加密管理系统(例如:Vault) NIY:凭证创建和转换控制器 2.2 应用层:部署和路由 应用管理和组合层,提供自愈、扩容、应用生命周期管理、服务发现、负载均衡和路由— 也即服务编排和service fabric。这些API和功能是所有Kubernetes分发所需要的,Kubernetes应该提供这些API的默认实现,当然可以使用替代的实现方案。没有应用层的API,大部分的容器化应用将不能运行。 Kubernetes’s API提供类似IaaS的以容器为中心的基础单元,以及生命周期控制器,以支持所有工作负载的编排(自愈、扩容、更新和终止)。这些应用管理、组合、发现、和路由API和功能包括: 默认调度,在Pod API中实现调度策略:资源请求、nodeSelector、node和pod affinity/anti-affinity、taints and tolerations. 调度能够作为一个独立的进度在集群内或外运行。 NIY:重新调度器 ,反应和主动删除已调度的POD,以便它们可以被替换并重新安排到其他Node 持续运行应用:这些应用类型应该能够通过声明式更新、级联删除、和孤儿/领养支持发布(回滚)。除了DaemonSet,应该能支持水平扩容。 The Deployment API,编排更新无状态的应用,包括子资源(状态、扩容和回滚) The DaemonSet API,集群服务,包括子资源(状态) The StatefulSet API,有状态应用,包括子资源(状态、扩容) The PodTemplate API,由DaemonSet和StatefulSet用来记录变更历史 终止批量应用:这些应该包括终止jobs的自动剔除(NIY) The Job API (GC discussion) The CronJob API 发现、负载均衡和路由 The Service API,包括集群IP地址分配,修复服务分配映射,通过kube-proxy或者对等的功能实现服务的负载均衡,自动化创建端点,维护和删除。NIY:负载均衡服务是可选的,如果被支持的化,则需要通过一致性的测试。 The Ingress API,包括internal L7 (NIY) 服务DNS。DNS使用official Kubernetes schema。 应用层可以依赖: 身份提供者 (集群的身份和/或应用身份) NIY:云提供者控制器实现 Ingress controller(s) 调度器和重新调度器的替代解决方案 DNS服务替代解决方案 kube-proxy替代解决方案 工作负载控制器替代解决方案和/或辅助,特别是用于扩展发布策略 2.3 治理层:自动化和策略执行 策略执行和高层自动化。这些API和功能是运行应用的可选功能,应该挺其它的解决方案实现。 每个支持的API/功能应用作为企业操作、安全和治理场景的一部分。 需要为集群提供可能的配置和发现默认策略,至少支持如下的用例: 单一租户/单一用户集群 多租户集群 生产和开发集群 Highly tenanted playground cluster 用于将计算/应用服务转售给他人的分段集群 需要关注的内容: 资源使用 Node内部分割 最终用户 管理员 服务质量(DoS) 自动化APIs和功能: 度量APIs (水平/垂直自动扩容的调度任务表) 水平Pod自动扩容API NIY:垂直Pod自动扩容API(s) 集群自动化扩容和Node供应 The PodDisruptionBudget API 动态存储卷供应,至少有一个出厂价来源类型 The StorageClass API,至少有一个默认存储卷类型的实现 动态负载均衡供应 NIY:PodPreset API NIY:service broker/catalog APIs NIY:Template和TemplateInstance APIs 策略APIs和功能: 授权:ABAC和RBAC授权策略方案 RBAC,实现下面的API:Role, RoleBinding, ClusterRole, ClusterRoleBinding The LimitRange API The ResourceQuota API The PodSecurityPolicy API The ImageReview API The NetworkPolicy API 管理层依赖: 网络策略执行机制 替换、水平和垂直Pod扩容 集群自动扩容和Node提供者 动态存储卷提供者 动态负载均衡提供者 度量监控pipeline,或者它的替换 服务代理 2.4 接口层:类库和工具 这些机制被建议用于应用程序版本的分发,用户也可以用其进行下载和安装。它们包括Kubernetes官方项目开发的通用的类库、工具、系统、界面,它们可以用来发布。 Kubectl — kubectl作为很多客户端工具中的一种,Kubernetes的目标是使Kubectl更薄,通过将常用的非平凡功能移动到API中。这是必要的,以便于跨Kubernetes版本的正确操作,并促进API的扩展性,以保持以API为中心的Kubernetes生态系统模型,并简化其它客户端,尤其是非GO客户端。 客户端类库(例如:client-go, client-python) 集群联邦(API server, controllers, kubefed) Dashboard Helm 这些组件依赖: Kubectl扩展 Helm扩展 2.5 生态 在有许多领域,已经为Kubernetes定义了明确的界限。虽然,Kubernetes必须提供部署和管理容器化应用需要的通用功能。但作为一般规则,在对Kubernete通用编排功能进行补足的功能领域,Kubernetes保持了用户的选择。特别是那些有自己的竞争优势的区域,特别是能够满足不同需求和偏好的众多解决方案。Kubernetes可以为这些解决方案提供插件API,或者可以公开由多个后端实现的通用API,或者公开此类解决方案可以针对的API。有时,功能可以与Kubernetes干净地组合在而不需要显式接口。 此外,如果考虑成为Kubernetes的一部分,组件就需要遵循Kubernetes设计约定。例如,主要接口使用特定域语言的系统(例如,Puppet、Open Policy Agent)与Kubenetes API的方法不兼容,可以与Kubernetes一起使用,但不会被认为是Kubernetes的一部分。类似地,被设计用来支持多平台的解决方案可能不会遵循Kubernetes API协议,因此也不会被认为是Kubernetes的一部分。 内部的容器镜像:Kubernetes不提供容器镜像的内容。 如果某些内容被设计部署在容器镜像中,则其不应该直接被考虑作为Kubernetes的一部分。例如,基于特定语言的框架。 在Kubernetes的顶部 持久化集成和部署(CI/CD):Kubernetes不提供从源代码到镜像的能力。Kubernetes 不部署源代码和不构建应用。用户和项目可以根据自身的需要选择持久化集成和持久化部署工作流,Kubernetes的目标是方便CI/CD的使用,而不是命令它们如何工作。 应用中间件:Kubernetes不提供应用中间件作为内置的基础设施,例如:消息队列和SQL数据库。然而,可以提供通用目的的机制使其能够被容易的提供、发现和访问。理想的情况是这些组件仅仅运行在Kubernetes上。 日志和监控:Kubernetes本身不提供日志聚合和综合应用监控的能力,也没有遥测分析和警报系统,虽然日志和监控的机制是Kubernetes集群必不可少的部分。 数据处理平台:在数据处理平台方面,Spark和Hadoop是还有名的两个例子,但市场中还存在很多其它的系统。 特定应用运算符:Kubernetes支持通用类别应用的工作负载管理。 平台即服务 Paas:Kubernetes为Paas提供基础。 功能即服务 FaaS:与PaaS类似,但Faa侵入容器和特定语言的应用框架。 工作量编排: “工作流”是一个非常广泛的和多样化的领域,通常针对特定的用例场景(例如:数据流图、数据驱动处理、部署流水线、事件驱动自动化、业务流程执行、iPAAS)和特定输入和事件来源的解决方案,并且通常需要通过编写代码来实现。 配置特定领域语言:特定领域的语言不利于分层高级的API和工具,它们通常具有有限的可表达性、可测试性、熟悉性和文档性。它们复杂的配置生成,它们倾向于在互操作性和可组合性间进行折衷。它们使依赖管理复杂化,并经常颠覆性的抽象和封装。 Kompose:Kompose是一个适配器工具,它有助于从Docker Compose迁移到Kubernetes ,并提供简单的用例。Kompose不遵循Kubernetes约定,而是基于手动维护的DSL。 ChatOps:也是一个适配器工具,用于聊天服务。 支撑Kubernetes 容器运行时:Kubernetes本身不提供容器运行时环境,但是其提供了接口,可以来插入所选择的容器运行时。 镜像仓库:Kubernetes本身不提供容器的镜像,可通过Harbor、Nexus和docker registry等搭建镜像仓库,以为集群拉取需要的容器镜像。 集群状态存储:用于存储集群运行状态,例如默认使用Etcd,但也可以使用其它存储系统。 网络:与容器运行时一样,Kubernetes提供了接入各种网络插件的容器网络接口(CNI)。 文件存储:本地文件系统和网络存储 Node管理:Kubernetes既不提供也不采用任何综合的机器配置、维护、管理或自愈系统。通常针对不同的公有/私有云,针对不同的操作系统,针对可变的和不可变的基础设施。 云提供者:IaaS供应和管理。 集群创建和管理:社区已经开发了很多的工具,利润minikube、kubeadm、bootkube、kube-aws、kops、kargo, kubernetes-anywhere等待。 从工具的多样性可以看出,集群部署和管理(例如,升级)没有一成不变的解决方案。也就是说,常见的构建块(例如,安全的Kubelet注册)和方法(特别是自托管)将减少此类工具中所需的自定义编排的数量。 后续,希望通过建立Kubernetes的生态系统,并通过整合相关的解决方案来满足上述需求。 3、矩阵管理 选项、可配置的默认、扩展、插件、附加组件、特定于提供者的功能、版本管理、特征发现和依赖性管理。 Kubernetes不仅仅是一个开源的工具箱,而且是一个典型集群或者服务的运行环境。 Kubernetes希望大多数用户和用例能够使用上游版本,这意味着Kubernetes需要足够的可扩展性,而不需要通过重建来处理各种场景。 虽然在可扩展性方面的差距是代码分支的主要驱动力,而上游集群生命周期管理解决方案中的差距是当前Kubernetes分发的主要驱动因素,可选特征的存在(例如,alpha API、提供者特定的API)、可配置性、插件化和可扩展性使概念不可避免。 然而,为了使用户有可能在Kubernetes上部署和管理他们的应用程序,为了使开发人员能够在Kubernetes集群上构建构建Kubernetes扩展,他们必须能够对Kubernetes集群或分发提供一个假设。在基本假设失效的情况下,需要找到一种方法来发现可用的功能,并表达功能需求(依赖性)以供使用。 集群组件,包括add-ons,应该通过组件注册 API进行注册和通过/componentstatuses进行发现。 启用内置API、聚合API和注册的第三方资源,应该可以通过发现和OpenAPI(Savigj.JSON)端点来发现。如上所述,LoadBalancer类型服务的云服务提供商应该确定负载均衡器API是否存在。 类似于StorageClass,扩展和它们的选项应该通过FoeClass资源进行注册。但是,使用参数描述、类型(例如,整数与字符串)、用于验证的约束(例如,ranger或regexp)和默认值,从扩展API中引用fooClassName。这些API应该配置/暴露相关的特征的存在,例如动态存储卷供应(由非空的storageclass.provisioner字段指示),以及标识负责的控制器。需要至少为调度器类、ingress控制器类、Flex存储卷类和计算资源类(例如GPU、其他加速器)添加这样的API。 假设我们将现有的网络存储卷转换为flex存储卷,这种方法将会覆盖存储卷来源。在将来,API应该只提供通用目的的抽象,即使与LoadBalancer服务一样,抽象并不需要在所有的环境中都实现(即,API不需要迎合最低公共特性)。 NIY:需要为注册和发现开发下面的机制: 准入控制插件和hooks(包括内置的APIs) 身份认证插件 授权插件和hooks 初始化和终结器 调度器扩展 Node标签和集群拓扑 NIY:单个API和细粒度特征的激活/失活可以通过以下机制解决: 所有组件的配置正在从命令行标志转换为版本化配置。 打算将大部分配置数据存储在配置映射(ConfigMaps)中,以便于动态重新配置、渐进发布和内省性。 所有/多个组件共同的配置应该被分解到它自己的配置对象中。这应该包括特征网关机制。 应该为语义意义上的设置添加API,例如,在无响应节点上删除Pod之前需要等待的默认时间长度。 NIY:版本管理操作的问题,取决于多个组件的升级(包括在HA集群中的相同组件的副本),应该通过以下方式来解决: 为所有新的特性创建flag网关 总是在它们出现的第一个小版本中,默认禁用这些特性, 提供启用特性的配置补丁; 在接下来的小版本中默认启用这些特性 NIY:我们还需要一个机制来警告过时的节点,和/或潜在防止Master升级(除了补丁发布),直到/除非Node已经升级。 NIY:字段级版本管理将有助于大量激活新的和/或alpha API字段的解决方案,防止不良写入过时的客户端对新字段的阻塞,以及非alpha API的演进,而不需要成熟的API定义的扩散。 Kubernetes API server忽略不支持的资源字段和查询参数,但不忽略未知的/未注册的API(注意禁用未实现的/不活动的API)。这有助于跨多个版本的集群重用配置,但往往会带来意外。Kubctl支持使用服务器的Wagger/OpenAPI规范进行可选验证。这样的可选验证,应该由服务器(NYY)提供。此外,为方便用户,共享资源清单应该指定Kubernetes版本最小的要求,这可能会被kubectl和其他客户端验证。 服务目录机制(NIY)应该能够断言应用级服务的存在,例如S3兼容的群集存储。 4、与安全相关的系统分层 为了正确地保护Kubernetes集群并使其能够安全扩展,一些基本概念需要由系统的组件进行定义和约定。最好从安全的角度把Kubernetes看作是一系列的环,每个层都赋予连续的层功能去行动。 用于存储核心API的一个或者多个数据存储系统(etcd) 核心APIs 高度可信赖资源的APIs(system policies) 委托的信任API和控制器(用户授予访问API /控制器,以代表用户执行操作),无论是在集群范围内还是在更小的范围内 在不同范围,运行不受信任/作用域API和控制器和用户工作负载 当较低层依赖于更高的层时,它会使安全模型崩溃,并使系统变得更加复杂。管理员可以选择这样做以获得操作简单性,但这必须是有意识的选择。一个简单的例子是etcd:可以将数据写入etcd的任何组件现在都在整个集群上,任何参与者(可以破坏高度信任的资源)都几乎可以进行逐步升级。为每一层的进程,将上面的层划分成不同的机器集(etcd-> apiservers +控制器->核心安全扩展->委托扩展- >用户工作负载),即使有些可能在实践中崩溃。 如果上面描述的层定义了同心圆,那么它也应该可能存在重叠或独立的圆-例如,管理员可以选择一个替代的秘密存储解决方案,集群工作负载可以访问,但是平台并不隐含地具有访问权限。这些圆圈的交点往往是运行工作负载的机器,并且节点必须没有比正常功能所需的特权更多的特权。 最后,在任何层通过扩展添加新的能力,应该遵循最佳实践来传达该行为的影响。 当一个能力通过扩展被添加到系统时,它有什么目的? 使系统更加安全 为集群中的每一个人,启用新的“生产质量”API 在集群的子集上自动完成一个公共任务 运行一个向用户提供API的托管工作负载(spark、数据库、etcd) 它们被分为三大类: 集群所需的(因此必须在内核附近运行,并在存在故障时导致操作权衡) 暴露于所有集群用户(必须正确地租用) 暴露于集群用户的子集(像传统的“应用程序”工作负载运行) 如果管理员可以很容易地被诱骗,在扩展期间安装新的群集级安全规则,那么分层被破坏,并且系统是脆弱的。
文章
存储  ·  Kubernetes  ·  安全  ·  API  ·  容器
2018-12-14
详解K8S系统架构演进过程与背后驱动的原因
1.背景   各种平台都会遇到一个不可回避的问题,即平台应该包含什么和不包含什么,Kubernetes也一样。Kubernetes作为一个部署和管理容器的平台,Kubernetes不能也不应该试图解决用户的所有问题。Kubernetes必须提供一些基本功能,用户可以在这些基本功能的基础上运行容器化的应用程序或构建它们的扩展。此文件旨在明确Kubernetes架构的设计意图,打算描述其演进和将来的开发蓝图。(NIY标记尚未实现的功能特性)   此文档用于描述Kubernetes系统的架构开发演进过程,以及背后的驱动原因。对于希望扩展或者定制kubernetes系统的开发者,其应该使用此文档作为向导,以明确可以在哪些地方,以及如何进行增强功能的实现。如果应用开发者需要开发一个大型的、便携和符合将来发展的kubernetes应用,也应该参考此文档,以了解Kubernetes将来的演化和发展。   从逻辑上来看,kubernetes的架构分为如下几个层次:   核心层(Nucleus): 提供标准的API和执行机制,包括基本的REST机制,安全、Pod、容器、网络接口和存储卷管理,通过接口能够对这些API和执进行扩展,核心层是必需的,它是系统最核心的一部分。   应用管理层(Application Management Layer ):提供基本的部署和路由,包括自愈能力、弹性扩容、服务发现、负载均衡和流量路由。此层即为通常所说的服务编排,这些功能都提供了默认的实现,但是允许进行一致性的替换。   治理层(The Governance Layer):提供高层次的自动化和策略执行,包括单一和多租户、度量、智能扩容和供应、授权方案、网络方案、配额方案、存储策略表达和执行。这些都是可选的,也可以通过其它解决方案实现。   接口层(The Interface Layer):提供公共的类库、工具、用户界面和与Kubernetes API交互的系统。   生态层(The Ecosystem):包括与Kubernetes相关的所有内容,严格上来说这些并不是Kubernetes的组成部分。包括CI/CD、中间件、日志、监控、数据处理、PaaS、serverless/FaaS系统、工作流、容器运行时、镜像仓库、Node和云提供商管理等。   2.系统分层   就像Linux拥有内核(kernel)、核心系统类库、和可选的用户级工具,kubernetes也拥有功能和工具的层次。对于开发者来说,理解这些层次是非常重要的。kubernetes APIs、概念和功能都在下面的层级图中得到体现。   2.1 核心层:API和执行(The Nucleus: API and Execution)   核心层包含最核心的API和执行机。   这些API和功能由上游的kubernetes代码库实现,由最小特性集和概念集所组成,这些特征和概念是系统上层所必需的。   这些由上游KubNeNETs代码库实现的API和函数包括建立系统的高阶层所需的最小特征集和概念集。这些内容被明确的地指定和记录,并且每个容器化的应用都会使用它们。开发人员可以安全地假设它们是一直存在的,这些内容应该是稳定和乏味的。   2.1.1 API和集群控制面板   Kubernetes集群提供了类似REST API的集,通过Kubernetes API server对外进行暴露,支持持久化资源的增删改查操作。这些API作为控制面板的枢纽。   遵循Kubernetes API约定(路径约定、标准元数据等)的REST API能够自动从共享API服务(认证、授权、审计日志)中收益,通用客户端代码能够与它们进行交互。   作为系统的最娣层,需要支持必要的扩展机制,以支持高层添加功能。另外,需要支持单租户和多租户的应用场景。核心层也需要提供足够的弹性,以支持高层能扩展新的范围,而不需要在安全模式方面进行妥协。   如果没有下面这些基础的API机和语义,Kubernetes将不能够正常工作:   认证(Authentication): 认证机制是非常关键的一项工作,在Kubernetes中需要通过服务器和客户端双方的认证通过。API server 支持基本认证模式 (用户命名/密码) (注意,在将来会被放弃), X.509客户端证书模式,OpenID连接令牌模式,和不记名令牌模式。通过kubeconfig支持,客户端能够使用上述各种认证模式。第三方认证系统可以实现TokenReview API,并通过配置认证webhook来调用,通过非标准的认证机制可以限制可用客户端的数量。   1、The TokenReview API (与hook的机制一样) 能够启用外部认证检查,例如Kubelet   2、Pod身份标识通过”service accounts“提供   3、The ServiceAccount API,包括通过控制器创建的默认ServiceAccount保密字段,并通过接入许可控制器进行注入。   授权(Authorization):第三方授权系统可以实现SubjectAccessReview API,并通过配置授权webhook进行调用。   1、SubjectAccessReview (与hook的机制一样), LocalSubjectAccessReview, 和SelfSubjectAccessReview APIs能启用外部的许可检查,诸如Kubelet和其它控制器。   REST 语义、监控、持久化和一致性保证、API版本控制、违约、验证   1、NIY:需要被解决的API缺陷:   2、混淆违约行为   3、缺少保障   4、编排支持   5、支持事件驱动的自动化   6、干净卸载   NIY: 内置的准入控制语义、同步准入控制钩子、异步资源初始化 — 发行商系统集成商,和集群管理员实现额外的策略和自动化   NIY:API注册和发行、包括API聚合、注册额外的API、发行支持的API、获得支持的操作、有效载荷和结果模式的详细信息。   NIY:ThirdPartyResource和ThirdPartyResourceData APIs (或她们的继承者),支持第三方存储和扩展API。   NIY:The Componentstatuses API的可扩展和高可用的替代,以确定集群是否完全体现和操作是否正确:ExternalServiceProvider (组件注册)   The Endpoints API,组件增持需要,API服务器端点的自我发布,高可用和应用层目标发行   The Namespace API,用户资源的范围,命名空间生命周期(例如:大量删除)   The Event API,用于对重大事件的发生进行报告,例如状态改变和错误,以及事件垃圾收集   NIY:级联删除垃圾收集器、finalization, 和orphaning   NIY: 需要内置的add-on的管理器 ,从而能够自动添加自宿主的组件和动态配置到集群,在运行的集群中提取出功能。   1、Add-ons应该是一个集群服务,作为集群的一部分进行管理   2、它们可以运行在kube-system命名空间,这么就不会与用户的命名进行冲突   API server作为集群的网关。根据定义,API server必需能够被集群外的客户端访问,而Node和Pod是不被集群外的客户端访问的。客户端认证API server,并使用API server作为堡垒和代理/通道来通过/proxy和/portforward API访问Node和Pod等Clients authenticate the API server and also use it   TBD:The CertificateSigningRequest API,能够启用认证创建,特别是kubele证书。   理想情况下,核心层API server江仅仅支持最小的必需的API,额外的功能通过聚合、钩子、初始化器、和其它扩展机制来提供。注意,中心化异步控制器以名为Controller Manager的独立进程运行,例如垃圾收集。   API server依赖下面的外部组件:   持久化状态存储 (etcd,或相对应的其它系统;可能会存在多个实例)   API server可以依赖:   身份认证提供者   The TokenReview API实现者 实现者   The SubjectAccessReview API实现者   2.1.2 执行   在Kubernetes中最重要的控制器是kubelet,它是Pod和Node API的主要实现者,没有这些API的话,Kubernetes将仅仅只是由键值对存储(后续,API机最终可能会被作为一个独立的项目)支持的一个增删改查的REST应用框架。   Kubernetes默认执行独立的应用容器和本地模式。   Kubernetes提供管理多个容器和存储卷的Pod,Pod在Kubernetes中作为最基本的执行单元。   Kubelet API语义包括:   The Pod API,Kubernetes执行单元,包括:   1、Pod可行性准入控制基于Pod API中的策略(资源请求、Node选择器、node/pod affinity and anti-affinity, taints and tolerations)。API准入控制可以拒绝Pod或添加额外的调度约束,但Kubelet才是决定Pod最终被运行在哪个Node上的决定者,而不是schedulers or DaemonSets。   2、容器和存储卷语义和生命周期   3、Pod IP地址分配(每个Pod要求一个可路由的IP地址)   4、将Pod连接至一个特定安全范围的机制(i.e., ServiceAccount)   5、存储卷来源:   5.1、emptyDir   5.2、hostPath   5.3、secret   5.4、configMap   5.5、downwardAPI   5.6、NIY:容器和镜像存储卷 (and deprecate gitRepo)   5.7、NIY:本地存储,对于开发和生产应用清单不需要复杂的模板或独立配置   5.8、flexVolume (应该替换内置的cloud-provider-specific存储卷)   6、子资源:绑定、状态、执行、日志、attach、端口转发、代理   NIY:可用性和引导API 资源检查点   容器镜像和日志生命周期   The Secret API,启用第三方加密管理   The ConfigMap API,用于组件配置和Pod引用   The Node API,Pod的宿主   1、在一些配置中,可以仅仅对集群管理员可见   Node和pod网络,业绩它们的控制(路由控制器)   Node库存、健康、和可达性(node控制器)   1、Cloud-provider-specific node库存功能应该被分成特定提供者的控制器   pod终止垃圾收集   存储卷控制器   1、Cloud-provider-specific attach/detach逻辑应该被分成特定提供者的控制器,需要一种方式从API中提取特定提供者的存储卷来源。   The PersistentVolume API   1、NIY:至少被本地存储所支持   The PersistentVolumeClaim API   中心化异步功能,诸如由Controller Manager执行的pod终止垃圾收集。   当前,控制过滤器和kubelet调用“云提供商”接口来询问来自于基础设施层的信息,并管理基础设施资源。然而,kubernetes正在努力将这些触摸点(问题)提取到外部组件中,不可满足的应用程序/容器/OS级请求(例如,PODS,PersistentVolumeClaims)作为外部“动态供应”系统的信号,这将使基础设施能够满足这些请求,并使用基础设施资源(例如,Node、和PersistentVolumes)在Kubernetes进行表示,这样Kubernetes可以将请求和基础设施资源绑定在一起。   对于kubelet,它依赖下面的可扩展组件:   镜像注册   容器运行时接口实现   容器网络接口实现   FlexVolume 实现(”CVI” in the diagram)   以及可能依赖:   NIY:第三方加密管理系统(例如:Vault)   NIY:凭证创建和转换控制器   2.2 应用层:部署和路由   应用管理和组合层,提供自愈、扩容、应用生命周期管理、服务发现、负载均衡和路由— 也即服务编排和service fabric。这些API和功能是所有Kubernetes分发所需要的,Kubernetes应该提供这些API的默认实现,当然可以使用替代的实现方案。没有应用层的API,大部分的容器化应用将不能运行。   Kubernetes’s API提供类似IaaS的以容器为中心的基础单元,以及生命周期控制器,以支持所有工作负载的编排(自愈、扩容、更新和终止)。这些应用管理、组合、发现、和路由API和功能包括:   默认调度,在Pod API中实现调度策略:资源请求、nodeSelector、node和pod affinity/anti-affinity、taints and tolerations. 调度能够作为一个独立的进度在集群内或外运行。   NIY:重新调度器 ,反应和主动删除已调度的POD,以便它们可以被替换并重新安排到其他Node   持续运行应用:这些应用类型应该能够通过声明式更新、级联删除、和孤儿/领养支持发布(回滚)。除了DaemonSet,应该能支持水平扩容。   1、The Deployment API,编排更新无状态的应用,包括子资源(状态、扩容和回滚)   2、The DaemonSet API,集群服务,包括子资源(状态)   3、The StatefulSet API,有状态应用,包括子资源(状态、扩容)   4、The PodTemplate API,由DaemonSet和StatefulSet用来记录变更历史   终止批量应用:这些应该包括终止jobs的自动剔除(NIY)   1、The Job API (GC discussion)   2、The CronJob API   发现、负载均衡和路由   1、The Service API,包括集群IP地址分配,修复服务分配映射,通过kube-proxy或者对等的功能实现服务的负载均衡,自动化创建端点,维护和删除。NIY:负载均衡服务是可选的,如果被支持的化,则需要通过一致性的测试。   2、The Ingress API,包括internal L7 (NIY)   4、服务DNS。DNS使用official Kubernetes schema。   应用层可以依赖:   身份提供者 (集群的身份和/或应用身份)   NIY:云提供者控制器实现   Ingress controller(s)   调度器和重新调度器的替代解决方案   DNS服务替代解决方案   kube-proxy替代解决方案   工作负载控制器替代解决方案和/或辅助,特别是用于扩展发布策略   2.3 治理层:自动化和策略执行   策略执行和高层自动化。这些API和功能是运行应用的可选功能,应该挺其它的解决方案实现。   每个支持的API/功能应用作为企业操作、安全和治理场景的一部分。   需要为集群提供可能的配置和发现默认策略,至少支持如下的用例:   单一租户/单一用户集群   多租户集群   生产和开发集群   Highly tenanted playground cluster   用于将计算/应用服务转售给他人的分段集群   需要关注的内容:   1、资源使用   2、Node内部分割   3、最终用户   4、管理员   5、服务质量(DoS)   自动化APIs和功能:   度量APIs (水平/垂直自动扩容的调度任务表)   水平Pod自动扩容API   NIY:垂直Pod自动扩容API(s)   集群自动化扩容和Node供应   The PodDisruptionBudget API   动态存储卷供应,至少有一个出厂价来源类型   1、The StorageClass API,至少有一个默认存储卷类型的实现   动态负载均衡供应   NIY:PodPreset API   NIY:service broker/catalog APIs   NIY:Template和TemplateInstance APIs   策略APIs和功能:   授权:ABAC和RBAC授权策略方案   1、RBAC,实现下面的API:Role, RoleBinding, ClusterRole, ClusterRoleBinding   The LimitRange API   The ResourceQuota API   The PodSecurityPolicy API   The ImageReview API   The NetworkPolicy API   管理层依赖:   网络策略执行机制   替换、水平和垂直Pod扩容   集群自动扩容和Node提供者   动态存储卷提供者   动态负载均衡提供者   度量监控pipeline,或者它的替换   服务代理   2.4 接口层:类库和工具   这些机制被建议用于应用程序版本的分发,用户也可以用其进行下载和安装。它们包括Kubernetes官方项目开发的通用的类库、工具、系统、界面,它们可以用来发布。   Kubectl — kubectl作为很多客户端工具中的一种,Kubernetes的目标是使Kubectl更薄,通过将常用的非平凡功能移动到API中。这是必要的,以便于跨Kubernetes版本的正确操作,并促进API的扩展性,以保持以API为中心的Kubernetes生态系统模型,并简化其它客户端,尤其是非GO客户端。   客户端类库(例如:client-go, client-python)   集群联邦(API server, controllers, kubefed)   Dashboard   Helm   这些组件依赖:   Kubectl扩展   Helm扩展   2.5 生态   在有许多领域,已经为Kubernetes定义了明确的界限。虽然,Kubernetes必须提供部署和管理容器化应用需要的通用功能。但作为一般规则,在对Kubernete通用编排功能进行补足的功能领域,Kubernetes保持了用户的选择。特别是那些有自己的竞争优势的区域,特别是能够满足不同需求和偏好的众多解决方案。Kubernetes可以为这些解决方案提供插件API,或者可以公开由多个后端实现的通用API,或者公开此类解决方案可以针对的API。有时,功能可以与Kubernetes干净地组合在而不需要显式接口。   此外,如果考虑成为Kubernetes的一部分,组件就需要遵循Kubernetes设计约定。例如,主要接口使用特定域语言的系统(例如,Puppet、Open Policy Agent)与Kubenetes API的方法不兼容,可以与Kubernetes一起使用,但不会被认为是Kubernetes的一部分。类似地,被设计用来支持多平台的解决方案可能不会遵循Kubernetes API协议,因此也不会被认为是Kubernetes的一部分。   内部的容器镜像:Kubernetes不提供容器镜像的内容。 如果某些内容被设计部署在容器镜像中,则其不应该直接被考虑作为Kubernetes的一部分。例如,基于特定语言的框架。   在Kubernetes的顶部   1、持久化集成和部署(CI/CD):Kubernetes不提供从源代码到镜像的能力。Kubernetes 不部署源代码和不构建应用。用户和项目可以根据自身的需要选择持久化集成和持久化部署工作流,Kubernetes的目标是方便CI/CD的使用,而不是命令它们如何工作。   2、应用中间件:Kubernetes不提供应用中间件作为内置的基础设施,例如:消息队列和SQL数据库。然而,可以提供通用目的的机制使其能够被容易的提供、发现和访问。理想的情况是这些组件仅仅运行在Kubernetes上。   3、日志和监控:Kubernetes本身不提供日志聚合和综合应用监控的能力,也没有遥测分析和警报系统,虽然日志和监控的机制是Kubernetes集群必不可少的部分。   4、数据处理平台:在数据处理平台方面,Spark和Hadoop是还有名的两个例子,但市场中还存在很多其它的系统。   5、特定应用运算符:Kubernetes支持通用类别应用的工作负载管理。   6、平台即服务 Paas:Kubernetes为Paas提供基础。   7、功能即服务 FaaS:与PaaS类似,但Faa侵入容器和特定语言的应用框架。   8、工作量编排: “工作流”是一个非常广泛的和多样化的领域,通常针对特定的用例场景(例如:数据流图、数据驱动处理、部署流水线、事件驱动自动化、业务流程执行、iPAAS)和特定输入和事件来源的解决方案,并且通常需要通过编写代码来实现。   9、配置特定领域语言:特定领域的语言不利于分层高级的API和工具,它们通常具有有限的可表达性、可测试性、熟悉性和文档性。它们复杂的配置生成,它们倾向于在互操作性和可组合性间进行折衷。它们使依赖管理复杂化,并经常颠覆性的抽象和封装。   10、Kompose:Kompose是一个适配器工具,它有助于从Docker Compose迁移到Kubernetes ,并提供简单的用例。Kompose不遵循Kubernetes约定,而是基于手动维护的DSL。   11、ChatOps:也是一个适配器工具,用于聊天服务。   支撑Kubernetes   1、容器运行时:Kubernetes本身不提供容器运行时环境,但是其提供了接口,可以来插入所选择的容器运行时。   2、镜像仓库:Kubernetes本身不提供容器的镜像,可通过Harbor、Nexus和docker registry等搭建镜像仓库,以为集群拉取需要的容器镜像。   3、集群状态存储:用于存储集群运行状态,例如默认使用Etcd,但也可以使用其它存储系统。   4、网络:与容器运行时一样,Kubernetes提供了接入各种网络插件的容器网络接口(CNI)。   5、文件存储:本地文件系统和网络存储   6、Node管理:Kubernetes既不提供也不采用任何综合的机器配置、维护、管理或自愈系统。通常针对不同的公有/私有云,针对不同的操作系统,针对可变的和不可变的基础设施。   7、云提供者:IaaS供应和管理。   8、集群创建和管理:社区已经开发了很多的工具,利润minikube、kubeadm、bootkube、kube-aws、kops、kargo, kubernetes-anywhere等待。 从工具的多样性可以看出,集群部署和管理(例如,升级)没有一成不变的解决方案。也就是说,常见的构建块(例如,安全的Kubelet注册)和方法(特别是自托管)将减少此类工具中所需的自定义编排的数量。   后续,希望通过建立Kubernetes的生态系统,并通过整合相关的解决方案来满足上述需求。   3.矩阵管理   选项、可配置的默认、扩展、插件、附加组件、特定于提供者的功能、版本管理、特征发现和依赖性管理。   Kubernetes不仅仅是一个开源的工具箱,而且是一个典型集群或者服务的运行环境。 Kubernetes希望大多数用户和用例能够使用上游版本,这意味着Kubernetes需要足够的可扩展性,而不需要通过重建来处理各种场景。   虽然在可扩展性方面的差距是代码分支的主要驱动力,而上游集群生命周期管理解决方案中的差距是当前Kubernetes分发的主要驱动因素,可选特征的存在(例如,alpha API、提供者特定的API)、可配置性、插件化和可扩展性使概念不可避免。   然而,为了使用户有可能在Kubernetes上部署和管理他们的应用程序,为了使开发人员能够在Kubernetes集群上构建构建Kubernetes扩展,他们必须能够对Kubernetes集群或分发提供一个假设。在基本假设失效的情况下,需要找到一种方法来发现可用的功能,并表达功能需求(依赖性)以供使用。   集群组件,包括add-ons,应该通过组件注册 API进行注册和通过/componentstatuses进行发现。   启用内置API、聚合API和注册的第三方资源,应该可以通过发现和OpenAPI(Savigj.JSON)端点来发现。如上所述,LoadBalancer类型服务的云服务提供商应该确定负载均衡器API是否存在。   类似于StorageClass,扩展和它们的选项应该通过FoeClass资源进行注册。但是,使用参数描述、类型(例如,整数与字符串)、用于验证的约束(例如,ranger或regexp)和默认值,从扩展API中引用fooClassName。这些API应该配置/暴露相关的特征的存在,例如动态存储卷供应(由非空的storageclass.provisioner字段指示),以及标识负责的控制器。需要至少为调度器类、ingress控制器类、Flex存储卷类和计算资源类(例如GPU、其他加速器)添加这样的API。   假设我们将现有的网络存储卷转换为flex存储卷,这种方法将会覆盖存储卷来源。在将来,API应该只提供通用目的的抽象,即使与LoadBalancer服务一样,抽象并不需要在所有的环境中都实现(即,API不需要迎合最低公共特性)。   NIY:需要为注册和发现开发下面的机制:   准入控制插件和hooks(包括内置的APIs)   身份认证插件   授权插件和hooks   初始化和终结器   调度器扩展   Node标签和集群拓扑   NIY:单个API和细粒度特征的激活/失活可以通过以下机制解决:   所有组件的配置正在从命令行标志转换为版本化配置。   打算将大部分配置数据存储在配置映射(ConfigMaps)中,以便于动态重新配置、渐进发布和内省性。   所有/多个组件共同的配置应该被分解到它自己的配置对象中。这应该包括特征网关机制。   应该为语义意义上的设置添加API,例如,在无响应节点上删除Pod之前需要等待的默认时间长度。   NIY:版本管理操作的问题,取决于多个组件的升级(包括在HA集群中的相同组件的副本),应该通过以下方式来解决:   为所有新的特性创建flag网关   总是在它们出现的第一个小版本中,默认禁用这些特性,   提供启用特性的配置补丁;   在接下来的小版本中默认启用这些特性   NIY:我们还需要一个机制来警告过时的节点,和/或潜在防止Master升级(除了补丁发布),直到/除非Node已经升级。   NIY:字段级版本管理将有助于大量激活新的和/或alpha API字段的解决方案,防止不良写入过时的客户端对新字段的阻塞,以及非alpha API的演进,而不需要成熟的API定义的扩散。   Kubernetes API server忽略不支持的资源字段和查询参数,但不忽略未知的/未注册的API(注意禁用未实现的/不活动的API)。这有助于跨多个版本的集群重用配置,但往往会带来意外。Kubctl支持使用服务器的Wagger/OpenAPI规范进行可选验证。这样的可选验证,应该由服务器(NYY)提供。此外,为方便用户,共享资源清单应该指定Kubernetes版本最小的要求,这可能会被kubectl和其他客户端验证。   服务目录机制(NIY)应该能够断言应用级服务的存在,例如S3兼容的群集存储。   4.与安全相关的系统分层   为了正确地保护Kubernetes集群并使其能够安全扩展,一些基本概念需要由系统的组件进行定义和约定。最好从安全的角度把Kubernetes看作是一系列的环,每个层都赋予连续的层功能去行动。   用于存储核心API的一个或者多个数据存储系统(etcd)   核心APIs   高度可信赖资源的APIs(system policies)   委托的信任API和控制器(用户授予访问API /控制器,以代表用户执行操作),无论是在集群范围内还是在更小的范围内   在不同范围,运行不受信任/作用域API和控制器和用户工作负载   当较低层依赖于更高的层时,它会使安全模型崩溃,并使系统变得更加复杂。管理员可以选择这样做以获得操作简单性,但这必须是有意识的选择。一个简单的例子是etcd:可以将数据写入etcd的任何组件现在都在整个集群上,任何参与者(可以破坏高度信任的资源)都几乎可以进行逐步升级。为每一层的进程,将上面的层划分成不同的机器集(etcd-> apiservers +控制器->核心安全扩展->委托扩展- >用户工作负载),即使有些可能在实践中崩溃。   如果上面描述的层定义了同心圆,那么它也应该可能存在重叠或独立的圆-例如,管理员可以选择一个替代的秘密存储解决方案,集群工作负载可以访问,但是平台并不隐含地具有访问权限。这些圆圈的交点往往是运行工作负载的机器,并且节点必须没有比正常功能所需的特权更多的特权。   最后,在任何层通过扩展添加新的能力,应该遵循最佳实践来传达该行为的影响。   当一个能力通过扩展被添加到系统时,它有什么目的?   使系统更加安全   为集群中的每一个人,启用新的“生产质量”API   在集群的子集上自动完成一个公共任务   运行一个向用户提供API的托管工作负载(spark、数据库、etcd)   它们被分为三大类:   1、集群所需的(因此必须在内核附近运行,并在存在故障时导致操作权衡)   2、暴露于所有集群用户(必须正确地租用)   3、暴露于集群用户的子集(像传统的“应用程序”工作负载运行)   如果管理员可以很容易地被诱骗,在扩展期间安装新的群集级安全规则,那么分层被破坏,并且系统是脆弱的。 原文发布时间为:2018-06-4 本文来自云栖社区合作伙伴“IT168”,了解相关信息可以关注“IT168”。
文章
存储  ·  Kubernetes  ·  安全  ·  API  ·  容器
2018-06-05
Kubernetes系统架构演进过程与背后驱动的原因
带你了解Kubernetes架构的设计意图、Kubernetes系统的架构开发演进过程,以及背后的驱动原因。------------ 1、背景各种平台都会遇到一个不可回避的问题,即平台应该包含什么和不包含什么,Kubernetes也一样。Kubernetes作为一个部署和管理容器的平台,Kubernetes不能也不应该试图解决用户的所有问题。Kubernetes必须提供一些基本功能,用户可以在这些基本功能的基础上运行容器化的应用程序或构建它们的扩展。本文旨在明确Kubernetes架构的设计意图,描述Kubernetes的演进历程和未来的开发蓝图。本文中,我们将描述Kubernetes系统的架构开发演进过程,以及背后的驱动原因。对于希望扩展或者定制kubernetes系统的开发者,其应该使用此文档作为向导,以明确可以在哪些地方,以及如何进行增强功能的实现。如果应用开发者需要开发一个大型的、可移植的和符合将来发展的kubernetes应用,也应该参考此文档,以了解Kubernetes将来的演化和发展。 从逻辑上来看,kubernetes的架构分为如下几个层次: 核心层(Nucleus): 提供标准的API和执行机制,包括基本的REST机制,安全、Pod、容器、网络接口和存储卷管理,通过接口能够对这些API和执进行扩展,核心层是必需的,它是系统最核心的一部分。应用管理层(Application Management Layer ):提供基本的部署和路由,包括自愈能力、弹性扩容、服务发现、负载均衡和流量路由。此层即为通常所说的服务编排,这些功能都提供了默认的实现,但是允许进行一致性的替换。治理层(The Governance Layer):提供高层次的自动化和策略执行,包括单一和多租户、度量、智能扩容和供应、授权方案、网络方案、配额方案、存储策略表达和执行。这些都是可选的,也可以通过其它解决方案实现。接口层(The Interface Layer):提供公共的类库、工具、用户界面和与Kubernetes API交互的系统。生态层(The Ecosystem):包括与Kubernetes相关的所有内容,严格上来说这些并不是Kubernetes的组成部分。包括CI/CD、中间件、日志、监控、数据处理、PaaS、serverless/FaaS系统、工作流、容器运行时、镜像仓库、Node和云提供商管理等。2、系统分层就像Linux拥有内核(kernel)、核心系统类库、和可选的用户级工具,kubernetes也拥有功能和工具的层次。对于开发者来说,理解这些层次是非常重要的。kubernetes APIs、概念和功能都在下面的层级图中得到体现。2.1 核心层:API和执行(The Nucleus: API and Execution) 核心层包含最核心的API和执行机。这些API和功能由上游的kubernetes代码库实现,由最小特性集和概念集所组成,这些特征和概念是系统上层所必需的。这些由上游KubNeNETs代码库实现的API和函数包括建立系统的高阶层所需的最小特征集和概念集。这些内容被明确的地指定和记录,并且每个容器化的应用都会使用它们。开发人员可以安全地假设它们是一直存在的,这些内容应该是稳定和乏味的。2.1.1 API和集群控制面板Kubernetes集群提供了类似REST API的集,通过Kubernetes API server对外进行暴露,支持持久化资源的增删改查操作。这些API作为控制面板的枢纽。遵循Kubernetes API约定(路径约定、标准元数据等)的REST API能够自动从共享API服务(认证、授权、审计日志)中收益,通用客户端代码能够与它们进行交互。作为系统的最娣层,需要支持必要的扩展机制,以支持高层添加功能。另外,需要支持单租户和多租户的应用场景。核心层也需要提供足够的弹性,以支持高层能扩展新的范围,而不需要在安全模式方面进行妥协。如果没有下面这些基础的API机和语义,Kubernetes将不能够正常工作:认证(Authentication): 认证机制是非常关键的一项工作,在Kubernetes中需要通过服务器和客户端双方的认证通过。API server 支持基本认证模式 (用户命名/密码) (注意,在将来会被放弃), X.509客户端证书模式,OpenID连接令牌模式,和不记名令牌模式。通过kubeconfig支持,客户端能够使用上述各种认证模式。第三方认证系统可以实现TokenReview API,并通过配置认证webhook来调用,通过非标准的认证机制可以限制可用客户端的数量。1、The TokenReview API (与hook的机制一样) 能够启用外部认证检查,例如Kubelet2、Pod身份标识通过”service accounts“提供3、The ServiceAccount API,包括通过控制器创建的默认ServiceAccount保密字段,并通过接入许可控制器进行注入。授权(Authorization):第三方授权系统可以实现SubjectAccessReview API,并通过配置授权webhook进行调用。1、SubjectAccessReview (与hook的机制一样), LocalSubjectAccessReview, 和SelfSubjectAccessReview APIs能启用外部的许可检查,诸如Kubelet和其它控制器。REST 语义、监控、持久化和一致性保证、API版本控制、违约、验证1、NIY:需要被解决的API缺陷:2、混淆违约行为3、缺少保障4、编排支持5、支持事件驱动的自动化6、干净卸载NIY: 内置的准入控制语义、同步准入控制钩子、异步资源初始化 — 发行商系统集成商,和集群管理员实现额外的策略和自动化NIY:API注册和发行、包括API聚合、注册额外的API、发行支持的API、获得支持的操作、有效载荷和结果模式的详细信息。NIY:ThirdPartyResource和ThirdPartyResourceData APIs (或她们的继承者),支持第三方存储和扩展API。NIY:The Componentstatuses API的可扩展和高可用的替代,以确定集群是否完全体现和操作是否正确:ExternalServiceProvider (组件注册)The Endpoints API,组件增持需要,API服务器端点的自我发布,高可用和应用层目标发行The Namespace API,用户资源的范围,命名空间生命周期(例如:大量删除)The Event API,用于对重大事件的发生进行报告,例如状态改变和错误,以及事件垃圾收集NIY:级联删除垃圾收集器、finalization, 和orphaningNIY: 需要内置的add-on的管理器 ,从而能够自动添加自宿主的组件和动态配置到集群,在运行的集群中提取出功能。1、Add-ons应该是一个集群服务,作为集群的一部分进行管理2、它们可以运行在kube-system命名空间,这么就不会与用户的命名进行冲突API server作为集群的网关。根据定义,API server必需能够被集群外的客户端访问,而Node和Pod是不被集群外的客户端访问的。客户端认证API server,并使用API server作为堡垒和代理/通道来通过/proxy和/portforward API访问Node和Pod等Clients authenticate the API server and also use itTBD:The CertificateSigningRequest API,能够启用认证创建,特别是kubele证书。理想情况下,核心层API server江仅仅支持最小的必需的API,额外的功能通过聚合、钩子、初始化器、和其它扩展机制来提供。注意,中心化异步控制器以名为Controller Manager的独立进程运行,例如垃圾收集。API server依赖下面的外部组件:持久化状态存储 (etcd,或相对应的其它系统;可能会存在多个实例)API server可以依赖:身份认证提供者The TokenReview API实现者 实现者The SubjectAccessReview API实现者2.1.2 执行在Kubernetes中最重要的控制器是kubelet,它是Pod和Node API的主要实现者,没有这些API的话,Kubernetes将仅仅只是由键值对存储(后续,API机最终可能会被作为一个独立的项目)支持的一个增删改查的REST应用框架。Kubernetes默认执行独立的应用容器和本地模式。Kubernetes提供管理多个容器和存储卷的Pod,Pod在Kubernetes中作为最基本的执行单元。Kubelet API语义包括:The Pod API,Kubernetes执行单元,包括:1、Pod可行性准入控制基于Pod API中的策略(资源请求、Node选择器、node/pod affinity and anti-affinity, taints and tolerations)。API准入控制可以拒绝Pod或添加额外的调度约束,但Kubelet才是决定Pod最终被运行在哪个Node上的决定者,而不是schedulers or DaemonSets。2、容器和存储卷语义和生命周期3、Pod IP地址分配(每个Pod要求一个可路由的IP地址)4、将Pod连接至一个特定安全范围的机制(i.e., ServiceAccount)5、存储卷来源:5.1、emptyDir5.2、hostPath5.3、secret5.4、configMap5.5、downwardAPI5.6、NIY:容器和镜像存储卷 (and deprecate gitRepo)5.7、NIY:本地存储,对于开发和生产应用清单不需要复杂的模板或独立配置5.8、flexVolume (应该替换内置的cloud-provider-specific存储卷)6、子资源:绑定、状态、执行、日志、attach、端口转发、代理 NIY:可用性和引导API 资源检查点 容器镜像和日志生命周期 The Secret API,启用第三方加密管理 The ConfigMap API,用于组件配置和Pod引用 The Node API,Pod的宿主 1、在一些配置中,可以仅仅对集群管理员可见 Node和pod网络,业绩它们的控制(路由控制器) Node库存、健康、和可达性(node控制器) 1、Cloud-provider-specific node库存功能应该被分成特定提供者的控制器 pod终止垃圾收集 存储卷控制器 1、Cloud-provider-specific attach/detach逻辑应该被分成特定提供者的控制器,需要一种方式从API中提取特定提供者的存储卷来源。The PersistentVolume API 1、NIY:至少被本地存储所支持The PersistentVolumeClaim API 中心化异步功能,诸如由Controller Manager执行的pod终止垃圾收集。当前,控制过滤器和kubelet调用“云提供商”接口来询问来自于基础设施层的信息,并管理基础设施资源。然而,kubernetes正在努力将这些触摸点(问题)提取到外部组件中,不可满足的应用程序/容器/OS级请求(例如,PODS,PersistentVolumeClaims)作为外部“动态供应”系统的信号,这将使基础设施能够满足这些请求,并使用基础设施资源(例如,Node、和PersistentVolumes)在Kubernetes进行表示,这样Kubernetes可以将请求和基础设施资源绑定在一起。对于kubelet,它依赖下面的可扩展组件: 镜像注册 容器运行时接口实现 容器网络接口实现 FlexVolume 实现(”CVI” in the diagram) 以及可能依赖: NIY:第三方加密管理系统(例如:Vault) NIY:凭证创建和转换控制器 2.2 应用层:部署和路由应用管理和组合层,提供自愈、扩容、应用生命周期管理、服务发现、负载均衡和路由— 也即服务编排和service fabric。这些API和功能是所有Kubernetes分发所需要的,Kubernetes应该提供这些API的默认实现,当然可以使用替代的实现方案。没有应用层的API,大部分的容器化应用将不能运行。Kubernetes’s API提供类似IaaS的以容器为中心的基础单元,以及生命周期控制器,以支持所有工作负载的编排(自愈、扩容、更新和终止)。这些应用管理、组合、发现、和路由API和功能包括: 默认调度,在Pod API中实现调度策略:资源请求、nodeSelector、node和pod affinity/anti-affinity、taints and tolerations. 调度能够作为一个独立的进度在集群内或外运行。 NIY:重新调度器 ,反应和主动删除已调度的POD,以便它们可以被替换并重新安排到其他Node 持续运行应用:这些应用类型应该能够通过声明式更新、级联删除、和孤儿/领养支持发布(回滚)。除了DaemonSet,应该能支持水平扩容。 1、The Deployment API,编排更新无状态的应用,包括子资源(状态、扩容和回滚)2、The DaemonSet API,集群服务,包括子资源(状态)3、The StatefulSet API,有状态应用,包括子资源(状态、扩容)4、The PodTemplate API,由DaemonSet和StatefulSet用来记录变更历史终止批量应用:这些应该包括终止jobs的自动剔除(NIY) 1、The Job API (GC discussion)2、The CronJob API发现、负载均衡和路由 1、The Service API,包括集群IP地址分配,修复服务分配映射,通过kube-proxy或者对等的功能实现服务的负载均衡,自动化创建端点,维护和删除。NIY:负载均衡服务是可选的,如果被支持的化,则需要通过一致性的测试。2、The Ingress API,包括internal L7 (NIY)3、服务DNS。DNS使用official Kubernetes schema。应用层可以依赖: 身份提供者 (集群的身份和/或应用身份) NIY:云提供者控制器实现 Ingress controller(s) 调度器和重新调度器的替代解决方案 DNS服务替代解决方案 kube-proxy替代解决方案 工作负载控制器替代解决方案和/或辅助,特别是用于扩展发布策略 2.3 治理层:自动化和策略执行策略执行和高层自动化。这些API和功能是运行应用的可选功能,应该挺其它的解决方案实现。每个支持的API/功能应用作为企业操作、安全和治理场景的一部分。需要为集群提供可能的配置和发现默认策略,至少支持如下的用例: 单一租户/单一用户集群 多租户集群 生产和开发集群 Highly tenanted playground cluster 用于将计算/应用服务转售给他人的分段集群 需要关注的内容:1、资源使用2、Node内部分割3、最终用户4、管理员5、服务质量(DoS)自动化APIs和功能: 度量APIs (水平/垂直自动扩容的调度任务表) 水平Pod自动扩容API NIY:垂直Pod自动扩容API(s) 集群自动化扩容和Node供应 The PodDisruptionBudget API 动态存储卷供应,至少有一个出厂价来源类型 1、The StorageClass API,至少有一个默认存储卷类型的实现 动态负载均衡供应 NIY:PodPreset API NIY:service broker/catalog APIs NIY:Template和TemplateInstance APIs 策略APIs和功能:授权:ABAC和RBAC授权策略方案1、RBAC,实现下面的API:Role, RoleBinding, ClusterRole, ClusterRoleBinding The LimitRange API The ResourceQuota API The PodSecurityPolicy API The ImageReview API The NetworkPolicy API 管理层依赖: 网络策略执行机制 替换、水平和垂直Pod扩容 集群自动扩容和Node提供者 动态存储卷提供者 动态负载均衡提供者 度量监控pipeline,或者它的替换 服务代理 2.4 接口层:类库和工具这些机制被建议用于应用程序版本的分发,用户也可以用其进行下载和安装。它们包括Kubernetes官方项目开发的通用的类库、工具、系统、界面,它们可以用来发布。 Kubectl — kubectl作为很多客户端工具中的一种,Kubernetes的目标是使Kubectl更薄,通过将常用的非平凡功能移动到API中。这是必要的,以便于跨Kubernetes版本的正确操作,并促进API的扩展性,以保持以API为中心的Kubernetes生态系统模型,并简化其它客户端,尤其是非GO客户端。 客户端类库(例如:client-go, client-python) 集群联邦(API server, controllers, kubefed) Dashboard Helm 这些组件依赖: Kubectl扩展 Helm扩展 2.5 生态在有许多领域,已经为Kubernetes定义了明确的界限。虽然,Kubernetes必须提供部署和管理容器化应用需要的通用功能。但作为一般规则,在对Kubernete通用编排功能进行补足的功能领域,Kubernetes保持了用户的选择。特别是那些有自己的竞争优势的区域,特别是能够满足不同需求和偏好的众多解决方案。Kubernetes可以为这些解决方案提供插件API,或者可以公开由多个后端实现的通用API,或者公开此类解决方案可以针对的API。有时,功能可以与Kubernetes干净地组合在而不需要显式接口。此外,如果考虑成为Kubernetes的一部分,组件就需要遵循Kubernetes设计约定。例如,主要接口使用特定域语言的系统(例如,Puppet、Open Policy Agent)与Kubenetes API的方法不兼容,可以与Kubernetes一起使用,但不会被认为是Kubernetes的一部分。类似地,被设计用来支持多平台的解决方案可能不会遵循Kubernetes API协议,因此也不会被认为是Kubernetes的一部分。 内部的容器镜像:Kubernetes不提供容器镜像的内容。 如果某些内容被设计部署在容器镜像中,则其不应该直接被考虑作为Kubernetes的一部分。例如,基于特定语言的框架。 在Kubernetes的顶部 1、持久化集成和部署(CI/CD):Kubernetes不提供从源代码到镜像的能力。Kubernetes 不部署源代码和不构建应用。用户和项目可以根据自身的需要选择持久化集成和持久化部署工作流,Kubernetes的目标是方便CI/CD的使用,而不是命令它们如何工作。2、应用中间件:Kubernetes不提供应用中间件作为内置的基础设施,例如:消息队列和SQL数据库。然而,可以提供通用目的的机制使其能够被容易的提供、发现和访问。理想的情况是这些组件仅仅运行在Kubernetes上。3、日志和监控:Kubernetes本身不提供日志聚合和综合应用监控的能力,也没有遥测分析和警报系统,虽然日志和监控的机制是Kubernetes集群必不可少的部分。4、数据处理平台:在数据处理平台方面,Spark和Hadoop是还有名的两个例子,但市场中还存在很多其它的系统。5、特定应用运算符:Kubernetes支持通用类别应用的工作负载管理。6、平台即服务 Paas:Kubernetes为Paas提供基础。7、功能即服务 FaaS:与PaaS类似,但Faa侵入容器和特定语言的应用框架。8、工作量编排: “工作流”是一个非常广泛的和多样化的领域,通常针对特定的用例场景(例如:数据流图、数据驱动处理、部署流水线、事件驱动自动化、业务流程执行、iPAAS)和特定输入和事件来源的解决方案,并且通常需要通过编写代码来实现。9、配置特定领域语言:特定领域的语言不利于分层高级的API和工具,它们通常具有有限的可表达性、可测试性、熟悉性和文档性。它们复杂的配置生成,它们倾向于在互操作性和可组合性间进行折衷。它们使依赖管理复杂化,并经常颠覆性的抽象和封装。10、Kompose:Kompose是一个适配器工具,它有助于从Docker Compose迁移到Kubernetes ,并提供简单的用例。Kompose不遵循Kubernetes约定,而是基于手动维护的DSL。11、ChatOps:也是一个适配器工具,用于聊天服务。支撑Kubernetes 1、容器运行时:Kubernetes本身不提供容器运行时环境,但是其提供了接口,可以来插入所选择的容器运行时。2、镜像仓库:Kubernetes本身不提供容器的镜像,可通过Harbor、Nexus和docker registry等搭建镜像仓库,以为集群拉取需要的容器镜像。3、集群状态存储:用于存储集群运行状态,例如默认使用Etcd,但也可以使用其它存储系统。4、网络:与容器运行时一样,Kubernetes提供了接入各种网络插件的容器网络接口(CNI)。5、文件存储:本地文件系统和网络存储6、Node管理:Kubernetes既不提供也不采用任何综合的机器配置、维护、管理或自愈系统。通常针对不同的公有/私有云,针对不同的操作系统,针对可变的和不可变的基础设施。7、云提供者:IaaS供应和管理。8、集群创建和管理:社区已经开发了很多的工具,利润minikube、kubeadm、bootkube、kube-aws、kops、kargo, kubernetes-anywhere等待。 从工具的多样性可以看出,集群部署和管理(例如,升级)没有一成不变的解决方案。也就是说,常见的构建块(例如,安全的Kubelet注册)和方法(特别是自托管)将减少此类工具中所需的自定义编排的数量。后续,希望通过建立Kubernetes的生态系统,并通过整合相关的解决方案来满足上述需求。矩阵管理选项、可配置的默认、扩展、插件、附加组件、特定于提供者的功能、版本管理、特征发现和依赖性管理。Kubernetes不仅仅是一个开源的工具箱,而且是一个典型集群或者服务的运行环境。 Kubernetes希望大多数用户和用例能够使用上游版本,这意味着Kubernetes需要足够的可扩展性,而不需要通过重建来处理各种场景。虽然在可扩展性方面的差距是代码分支的主要驱动力,而上游集群生命周期管理解决方案中的差距是当前Kubernetes分发的主要驱动因素,可选特征的存在(例如,alpha API、提供者特定的API)、可配置性、插件化和可扩展性使概念不可避免。然而,为了使用户有可能在Kubernetes上部署和管理他们的应用程序,为了使开发人员能够在Kubernetes集群上构建构建Kubernetes扩展,他们必须能够对Kubernetes集群或分发提供一个假设。在基本假设失效的情况下,需要找到一种方法来发现可用的功能,并表达功能需求(依赖性)以供使用。集群组件,包括add-ons,应该通过组件注册 API进行注册和通过/componentstatuses进行发现。启用内置API、聚合API和注册的第三方资源,应该可以通过发现和OpenAPI(Savigj.JSON)端点来发现。如上所述,LoadBalancer类型服务的云服务提供商应该确定负载均衡器API是否存在。类似于StorageClass,扩展和它们的选项应该通过FoeClass资源进行注册。但是,使用参数描述、类型(例如,整数与字符串)、用于验证的约束(例如,ranger或regexp)和默认值,从扩展API中引用fooClassName。这些API应该配置/暴露相关的特征的存在,例如动态存储卷供应(由非空的storageclass.provisioner字段指示),以及标识负责的控制器。需要至少为调度器类、ingress控制器类、Flex存储卷类和计算资源类(例如GPU、其他加速器)添加这样的API。假设我们将现有的网络存储卷转换为flex存储卷,这种方法将会覆盖存储卷来源。在将来,API应该只提供通用目的的抽象,即使与LoadBalancer服务一样,抽象并不需要在所有的环境中都实现(即,API不需要迎合最低公共特性)。NIY:需要为注册和发现开发下面的机制: 准入控制插件和hooks(包括内置的APIs) 身份认证插件 授权插件和hooks 初始化和终结器 调度器扩展 Node标签和集群拓扑 NIY:单个API和细粒度特征的激活/失活可以通过以下机制解决: 所有组件的配置正在从命令行标志转换为版本化配置。 打算将大部分配置数据存储在配置映射(ConfigMaps)中,以便于动态重新配置、渐进发布和内省性。 所有/多个组件共同的配置应该被分解到它自己的配置对象中。这应该包括特征网关机制。 应该为语义意义上的设置添加API,例如,在无响应节点上删除Pod之前需要等待的默认时间长度。 NIY:版本管理操作的问题,取决于多个组件的升级(包括在HA集群中的相同组件的副本),应该通过以下方式来解决:为所有新的特性创建flag网关总是在它们出现的第一个小版本中,默认禁用这些特性,提供启用特性的配置补丁;在接下来的小版本中默认启用这些特性NIY:我们还需要一个机制来警告过时的节点,和/或潜在防止Master升级(除了补丁发布),直到/除非Node已经升级。NIY:字段级版本管理将有助于大量激活新的和/或alpha API字段的解决方案,防止不良写入过时的客户端对新字段的阻塞,以及非alpha API的演进,而不需要成熟的API定义的扩散。Kubernetes API server忽略不支持的资源字段和查询参数,但不忽略未知的/未注册的API(注意禁用未实现的/不活动的API)。这有助于跨多个版本的集群重用配置,但往往会带来意外。Kubctl支持使用服务器的Wagger/OpenAPI规范进行可选验证。这样的可选验证,应该由服务器(NYY)提供。此外,为方便用户,共享资源清单应该指定Kubernetes版本最小的要求,这可能会被kubectl和其他客户端验证。服务目录机制(NIY)应该能够断言应用级服务的存在,例如S3兼容的群集存储。与安全相关的系统分层为了正确地保护Kubernetes集群并使其能够安全扩展,一些基本概念需要由系统的组件进行定义和约定。最好从安全的角度把Kubernetes看作是一系列的环,每个层都赋予连续的层功能去行动。 用于存储核心API的一个或者多个数据存储系统(etcd) 核心APIs 高度可信赖资源的APIs(system policies) 委托的信任API和控制器(用户授予访问API /控制器,以代表用户执行操作),无论是在集群范围内还是在更小的范围内 在不同范围,运行不受信任/作用域API和控制器和用户工作负载 当较低层依赖于更高的层时,它会使安全模型崩溃,并使系统变得更加复杂。管理员可以选择这样做以获得操作简单性,但这必须是有意识的选择。一个简单的例子是etcd:可以将数据写入etcd的任何组件现在都在整个集群上,任何参与者(可以破坏高度信任的资源)都几乎可以进行逐步升级。为每一层的进程,将上面的层划分成不同的机器集(etcd-> apiservers +控制器->核心安全扩展->委托扩展- >用户工作负载),即使有些可能在实践中崩溃。如果上面描述的层定义了同心圆,那么它也应该可能存在重叠或独立的圆-例如,管理员可以选择一个替代的秘密存储解决方案,集群工作负载可以访问,但是平台并不隐含地具有访问权限。这些圆圈的交点往往是运行工作负载的机器,并且节点必须没有比正常功能所需的特权更多的特权。最后,在任何层通过扩展添加新的能力,应该遵循最佳实践来传达该行为的影响。当一个能力通过扩展被添加到系统时,它有什么目的?使系统更加安全为集群中的每一个人,启用新的“生产质量”API在集群的子集上自动完成一个公共任务运行一个向用户提供API的托管工作负载(spark、数据库、etcd)它们被分为三大类:1、集群所需的(因此必须在内核附近运行,并在存在故障时导致操作权衡)2、暴露于所有集群用户(必须正确地租用)3、暴露于集群用户的子集(像传统的“应用程序”工作负载运行)如果管理员可以很容易地被诱骗,在扩展期间安装新的群集级安全规则,那么分层被破坏,并且系统是脆弱的。 本文转自DockOne-Kubernetes系统架构演进过程与背后驱动的原因
文章
存储  ·  Kubernetes  ·  安全  ·  API  ·  容器
2018-12-18
使用 Windows XP 的两种强大的工具在您的代码中检测并堵塞 GDI 泄漏
在以前的一篇文章中,作者设计了一种简单的方法来检测图形设备接口 (GDI) 对象,这些对象并未由 Windows 9x 平台上基于 Win32 的应用程序正确地进行发布。因为有些更新版本的 Windows 需要一种不太相同的 GDI 泄漏方法,作者已经更新了针对那些操作系统的方法。他构建并说明了两种工具,这两种工具旨在检测并消除在 Windows XP、Windows 2000 和 Windows NT 上运行的应用程序中的 GDI 泄漏。 在 Windows® 95、Windows 98 和 Windows Me 中,图形设备接口 (GDI) 句柄是一个 16 位的值,任何应用程序都可以使用它来调用 GDI API 的函数。在 2001 年 3 月一期的 MSDN® Magazine 中,我讲述了如何利用这些平台的 16 位特性来构建 GDIUsage,这是一种所有应用程序都可使用的列出、比较并显示 GDI 对象的工具(参见“Resource Leaks:Detecting, Locating, and Repairing Your Leaky GDI Code”)。本文将说明如何编写用于 Windows XP 的同种类型的工具。我这里将要使用的方法同样很好地适用于 Windows 2000 和 Windows NT® 4.0,但出于本文的目的,我将使用 Windows XP 来表示所有这三种平台。 图 1 Windows 2000 中 GDI 的使用 本文说明了 Windows 9x 和 Windows XP 平台的不同,提出了在工具的实现过程中产生问题的解决方案,您可以在图 1 中看到该工具。我将解释如何利用代码插入机制来确定某个进程的 GDI 资源消耗情况,以及如何修补进程或 DLL,使其在创建 GDI 对象时得到通知。接下来,我将说明如何编写 Win32® 调试器来驱动某一进程,如何让该进程和调试器彼此之间进行通信,以及如何实现调用堆栈管理器来提供有关 GDI 对象资源分配的额外信息。 Windows 9x 与 Windows XP 对于 Windows XP,一系列 GDI 对象均与各个进程相关,大部分由 win32k.sys 按内核模式进行托管,设备驱动程序负责 USER 和 GDI 实现。Win32 应用程序通过由 user32.dll 和 gdi32.dll 提供的 API 调用这些系统服务。因为 Windows 基于每个进程保留了 GDI 对象的记录,所以只有创建了 GDI 对象的应用程序能够使用该对象对应的 GDI 函数。 Windows 9x 版本的 GDIUsage 使用 GetObjectType API 函数,该函数提供给定句柄值的 GDI 对象的类型,以检查某个随机值是否是个有效的 GDI 句柄。然而,与 Windows 9x 不同,Windows XP GDI 对象句柄完全是个 32 位的值。其可能的范围是从 0 到 0xFFFFFFFF,为了列出所有真实的 GDI 对象,各个可能的句柄值都需提供给 GetObjectType。这就造成了实际的性能问题。下列代码需要几分钟来执行,遗憾的是,很多通过运行测试应用程序检测到的 GDI 对象都不是真的(其中有 500 多个!): DWORD dwObjectType; DWORD hGdi; for (hGdi = 0; hGdi < 0xFFFFFFFF; hGdi++) { dwObjectType = ::GetObjectType((HANDLE)hGdi); if (dwObjectType != 0) { TRACE("0x%08x -> %u\n", hGdi, dwObjectType); } } 这意味着还需要有使用 GetObject 的额外测试代码(可能会很长)来获得一个可靠的列表。循环周期使得该方法不可用。本文提供了两种其他的解决方案。第一种方案使用 GDI 管理的句柄表,而第二种方案通过挂钩来自 GDI API 的函数进行工作。值得一提的是,第一种解决方案在未来的 OS 版本中既得不到支持也不能保证具有相同的行为。找到另一种可获得真正 GDI 对象列表的方法是要解决的首要问题。 对于 Windows 9x 的 GDIUsage,有可能利用对应于每个 GDI 对象类型(如位图的 BitBlt 或画笔的 FillRect)的函数来显示 GDI 对象。但是由于当利用由另一个进程创建的 GDI 对象来调用这些 API 函数时会失败,我所开发的工具的主要功能(即能够“看到”正在泄漏的资源)消失了。另一方面,显示的代码在创建 GDI 对象的应用程序中运行正常。解决方案是显而易见的 — 显示引擎必须运行于其他进程的上下文中。本文的稍后部分将讲述一种基于将 Windows 挂钩作为进程间通信机制的实现。 最后,用 Win32 调试 API 将这些方法结合起来,因此,就获得了可以运行于 Windows XP 和其他 32 位 Windows 平台上的 GDIUsage 版本的实现。 GDI 如何管理句柄 在我 2002 年 8 月 的文章中,WinDBG 用来说明进程环境块 (PEB) 结构。在那篇文章中,GdiSharedHandleTable 字段应引起您的注意,该字段如图 2 转载所示。事实上,这是一个指向表的指针,其中 GDI 存储了它的句柄,甚至那些由其他进程创建的句柄。在他撰写的 Windows Graphics Programming:Win32 GDI and DirectDraw (Prentice Hall,2002 年)一书中,Feng Huan 提供了另一种访问该表的方法,但他也描述了该表中每个 0x4000 项的结构,如下所示: typedef struct { DWORD pKernelInfo; // 2000/XP layout but these fields are inverted in Windows NT WORD ProcessID; WORD _nCount; WORD nUpper; WORD nType; DWORD pUserInfo; } GDITableEntry; 每一项都存储了 GDI 句柄的详细信息,句柄的值很容易计算。它的低 16 位是在表中的索引,其高 16 位保存在 nUpper 字段中。顾名思义,ProcessID 字段包含创建对象的进程 ID。有了这些信息,简单的循环就可以允许您列出某个特定进程正在使用的对象,而这也正是 GDIndicator 所做的事情,如图 3 所示。 如果您有兴趣了解获得运行进程列表的不同方法,您可以阅读我在 2002 年 6 月一期上发表的文章。每个进程都有一个 ID,用来从共享的表中收集该进程使用的 GDI 对象,并利用 ProcessID 字段进行比较。得到的计数值显示在每个对象类型列下的 GDIndicator 中。 与其他列不同,第三列显示两个值。第一个值是调用 GetGuiResources 的结果(这应该返回该进程使用的 GUI 对象句柄的计数值),第二个加括号的值是在解析 GDI 句柄共享表的过程中得到的和。您可以在图 3 中看到,这两个值通常是不同的,而 GetGuiResources 总是返回较大的计数值。没有文献说明这种不同的原因,与常用对象或未发布对象也没有什么明显的关系。有可能是在您背后分配给 GDI 没有存储在共享表中的对象,因此是您没有涉及的对象。 这种隐藏分配的一个例子发生在图标操作的过程中。当您创建或加载某个图标时,Windows 需要多个位图来实现透明效果。通常一个用于掩码,一个用于可视图形。与位图不同,图标由 USER 系统组件来处理,而不是由 GDI 来处理。这可能就是当调用 GetGuiResources 来了解 GDI 的使用情况时 GetGuiResources 背后的代码好像没有跟踪这些分配的原因。 通过 API 挂钩来跟踪对象分配 您已经看到,要了解特定的过程使用哪些 GDI 对象并不容易。怎样才能知道对象是否已由应用程序代码或背后的 GDI 自身加以分配呢?如果创建 GDI 对象时 Windows 能够通知您,那么就很容易存储它的句柄值并构建由应用程序分配的对象列表。遗憾的是,Win32 API 并没有为开发人员提供这种通知机制。 如果您想知道何时创建了新对象,必须了解图 4 中列出的函数调用。 幸运的是,在作者的文章“Learn System-Level Win32 Coding Techniques by Writing an API Spy Program”中(发表于 1994 年 12 月一期的 MSJ),Matt Pietrek 说明了如何编写 Win32 领域的 API 侦探引擎。给定一个特定模块(进程或 DLL),该引擎可以用您自己的函数地址替换被调用函数(由 DLL 导出)的地址。一旦执行了这种替换,每次被侦探的模块调用一个挂钩函数时,将在其所在位置执行您自己的句柄。 该 API 挂钩原则已经过多年的改进(参见 1998 年 2 月和 1999 年 6 月期 MSJ 的 John Robbins Bugslayer 专栏。)如果您需要了解不同 Windows 平台的可能实现,应该阅读 Jeffrey Richter 的 Programming Applications For Microsoft Windows Fourth Edition“(Microsoft® 出版社,1999 年)一书的第 22 章,以及 John Robbins 的 Debugging Applications(Microsoft 出版社,2000 年)一书。这里我使用了 John Robbins 的方法。 图 5 调用 GetDC 的内存布局 John 的 HookImportedFunctionsByName helper 函数接受修补函数列表、导出它们的系统 DLL 的加载地址、调用被修补函数的模块,以及要重新定向到的存根列表。关于退出,它填充了包含所有被修补函数地址的列表。例如,如果 App.exe 正从 USSER32.DLL 调用 GetDC,则您将得到如图 5 所示的内存布局。如果我用下面的输入参数调用 HookImportedFunctionsByName,它将产生如图 6 所示的不同布局。 • 系统修补函数列表 (GetDC) • 导出函数的 DLL 的地址 (USER32.dll) • 模块调用(App.exe 直接调用 GetDC) • 修补函数的列表(来自于 Hook.dll 的 GetDC) 在该特例中,包含所有已修补函数地址的列表应是 initial@。 图 6 对 GetDC 修补调用的内存布局 除了图 4 中列出的每个函数调用外,用同一机制来挂钩自由的 GDI 对象的函数(如图 7 所示)。 有了这两种类型的通知,您就有可能跟踪运行的活动 GDI 对象。 CGDIReflect 类负责提供静态的存根方法,这些方法将替代系统函数被调用。该类派生于 CAPIReflect,其主要目标是利用宏将给定模块的函数调用重新定向到静态类成员中。这种替换是通过对 DoReflect 的调用来完成的,DoReflect 接受调用方模块句柄作为参数。派生类的作用就是将您有兴趣接收有关信息的每个系统函数映射到适当的存根函数,本文后面的部分将对此进行讨论。 遵循消息映射机制,定义了一组宏来帮助您自动定义并声明存根函数。在 /P 编译器选项的帮助下,可获得每个包含所有扩展宏代码的源文件的 .i file。您需要观察结果文件的大小,该结果文件可能很大,但这种方法准确地显示了哪些代码被执行,您将在本文后面的图和源代码中看到。 从 USER 和 GDI 重定向函数需要三步,稍后我将对这三步进行概述。使用同一示例,我将讲述如何修补 user.dll 中的 GetDC。 第一步是利用 DECLARE_REFLECT_APIxxx 宏声明 CGDIReflect 中的静态变量,其中 xxx 表示该函数的参数数量(GetDC 有 1个,它接受 HWND 作为参数)。该声明用 BEGIN_REFLECT_LIST 和 END_REFLECT_LIST 框起来,前者定义了一个隐藏的、提供跟踪服务的 TraceReflectCall helper 方法,后者没起什么作用: BEGIN_REFLECT_LIST() DECLARE_REFLECT_API1(GetDC, HDC, HWND, hWnd) END_REFLECT_LIST() 但是,DECLARE_REFLECT_API 宏需要提供系统函数名、其返回类型及其参数列表(类型和名称)。提供这些信息允许将宏扩展到 CGDIReflect(实际上是个存根)的静态方法中,该方法共享同一原型并执行下列步骤。首先,通过别名调用初始的系统函数(后来由DEFINE_API_REFLECT 实例化)。之后,由前一调用分配的句柄及其类型被存储到一个 CHandleInfo 结构中(参见图 8),这是供将来使用的一些额外数据(参见下一部分有关 DoStackAddressDump 的讨论)。最后,CGDIReflect 的静态映射成员被更新,从而用前面讲到的结构来与新分配的句柄以及想俘获的创建对象相关联。 第二步通过下列宏实现每个静态成员,作为挂钩系统函数地址的别名: DEFINE_API_REFLECT(CGDIReflect, GetDC); 在我的示例中,宏可扩展到: CGDIReflect::GetDCProc CGDIReflect::__GetDC = 0; 在第三步和最后一步,需要实例化所有这些成员,然后在执行时使用。在这两种情况下都要调用 FillStubs 方法。首先,将 APIR STATE INIT 作为参数,由 Init调用 FillStubs,并且没有模块句柄要修补。这在利用 GetProcAddress 计算系统函数的地址时发生,然后地址存储在别名成员(对于 __GetDC 为 GetDC)中。 接着,在新的 DLL 需要修补时调用 FillStubs。用 APIR STATE ENABLE 作为参数,DoReflect调用 FillStubs。它将由模块进行的每个系统调用 (GetDC) 重定向到相应的静态存根方法(本示例中为 _GetDC),而模块的加载地址作为参数被传递。该方法遵循与 MFC 消息映射相同的模式:BEGIN_IMPLEMENT_API_REFLECT() ••• IMPLEMENT_API_REFLECT(hModule, "USER32.DLL", GetDC); ••• END_IMPLEMENT_API_REFLECT() 为了在执行过程中帮助调试宏,一些 AfxTrace 调用分散在扩展代码中。根据图 9 中所示的值,SetTraceLevel 允许您选择跟踪哪个操作。 现在有了 CGDIReflect 类,它允许您将由特定模块发出的任一调用重定向到存根方法,该方法的唯一作用是将新建的 GDI 对象存储到映射中。但是该实现有一个缺陷 — 它不是线程安全的。如果您需要检查的应用程序是多线程的,几个线程都在调用 GDI 函数,产生的行为可能不确定,因为跨线程访问句柄映射是不同步的。 利用堆栈跟踪监视分配 每次对重要函数的调用 都终止于一个存根,该存根完成两项操作。第一项是将分配的句柄及其类型包装到 GDIReflect.h 中声明的一个 CHandleInfo 对象中。第二项操作更有趣。在 CHandleInfo 对象中,当前调用堆栈的每个函数地址都存储在 m_pStack 中 — 分配的 DWORDs 数组 — 而 m_pStack 中保存的地址数保存在 m Depth 中。因此,除了以图形方式表示 GDI 对象外,还可能显示导致分配特定 GDI 对象的函数调用堆栈,如图 10 所示。 图 10 导致分配对象的单元 当需要浏览堆栈时,imagehlp.dll 和 dbghelp.dll 是您最好的朋友。为了便于您的使用,John Robbins 已经将该引擎包装到了 CSymbolEngine 类中,该类的发展历程在 1998 年 4 月和 1999 年 2 月的 MSJBugslayer 专栏中有所介绍。在 John Robbins 的 Debugging Applications 一书中详细地介绍了使用了 DBGHELP 的最后一个版本,该书我在前面提到过。 CSymbolEngine 类是一个在由 DBGHELP 导出的许多函数顶部的低级层。StackManager.cpp(位于本文顶部链接处代码下载中的 /Common 目录内)中实现的 CstackManager 提供了只有三个有趣方法的更高级功能。第一个是 DoStackAddressDump,它利用 CSymbolEngine 分配并填充一组当前的调用堆栈。该方法由每个存根调用,并存储导致对象分配的每个函数地址。 十六进制地址对计算机有好处,但对人没什么好处。为了将由 DoStackAddressDump 返回的地址数组转换为可读的格式,如图 10 所示,必需调用 DumpStackAllocation。该方法接受堆栈转储及其深度,然后在一个 CString 中返回转换后的堆栈。该方法的调用方能够选择他希望在每个地址间使用的行分隔符,要么是选择 \r\n 在编辑框中显示 CString,要么是选择 \n,利用 Trace 或 OutputDebugString简单地将其进行记录。该方法背后并没有什么魔法,对于给定数组中的每个地址,它都调用 ConvertStackAddressIntoFunctionName。 魔法在其他地方存在。当堆栈由 DoStackAddressDump 转储,而地址存储于返回的数组中时,该方法还可以利用 CSymbolEngine 中定义的 SymGetModuleInfo、SymGetSymFromAddr 和 SymGetLineFromAddr(有关实现细节,请参见代码下载中 StackManager.cpp 中的 ConvertAddress)找到对应于地址的符号。为什么现在进行转换?答案很简单:在这个特定的时刻,您确信相应的 DLL 被加载,但稍后调用 DumpStackAllocation 时情况可能会不一样。 如果频繁创建 GDI 对象,就会产生许多堆栈转储,并保存在 m_HandleMap 中。但保存在该映射中的 CHandleInfo 对象保留的是地址数组,而不是转换后的字符串数组。技巧是利用一个映射成员(如 m_AddressToName)来跟踪转换。这就避免了在堆栈转储中存储长字符串来代替每个地址的 DWORD 类型,因此减少了对内存的消耗。另一个好处是,堆栈转储运行速度会更快,因为利用 m_AddressToName 来作为缓冲区,从而避免了对符号引擎进行查询。 即使您知道如何挂钩一系列 GDI 函数,您仍需要了解在哪些模块中调用这些 GDI。我们说,在共享 DLL 中使用 MFC 的应用程序正在创建一个 Cpen 对象,从而操作与 Windows 画笔相关的 API。真正调用 CreatePen(它返回画笔的句柄)是在 MFC DLL 中(而不是在调用应用程序代码中)完成的。如果只挂钩由可执行文件调用的 API 函数,就会丢失来自由应用程序使用的所有 DLL 的调用。 通过调试来确定需要修补的 DLL 在 Windows XP 中,获得在给定时间由某个进程加载的所有 DLL 的列表非常简单,这要感谢 PSAPI 函数 EnumProcessModules,正如 Matt Pietrek 在“Under the Hood”一文中所述,该文发表在 1996 年 8 月发行的 MSJ 中。但是,对于动态加载的库,这个问题需要一点窍门。除了挂钩系统函数,还必须挂钩主程序发出(ANSI 与 UNICODE 版本)的 LoadLibrary 调用,从而检测何时加载了一个新 DLL,并递归地对其进行相同的挂钩处理。 需要回答最后的两个问题。第一,如何知道在侦探的进程中哪些 DLL 需要修补?第二,如何确保这些代码在另一个进程中执行?如果有这样的解决方案,那么还有可能用它来显示任何 GDI 对象句柄的图形化表示。在2002 年 8 月发表的有关调试的文章中,一个调试过的进程通过使用 Win32 调试 API 来动态或静态地检测加载的 DLL。这种方法的主要缺点是需要启动和调试应用程序。与 Windows 9x 版本的 GDIUsage 不同,被检测的 GDI 对象必须是由调试过的应用程序分配的对象,这可以在图 1 中看到。 Win32 调试 API 允许您编写代码来启动某个应用程序(调试对象),并且当发生事件时(如加载一个新的 DLL)获得通知。这正是您所需要的。要轻松地编写调试器,您只需重载在调试事件发生时要调用的虚方法。CGDIDebugger 类派生于 CapplicationDebugger,在我的 2002 年 8 月的文章中介绍过 CapplicationDebugger。图 11 显示了 CGDIDebugger 所重载方法的名称,并解释了每个方法的作用。稍后我将讨论调试器和调试对象之间的通信机制。 图 12 搜索字符串 除了这些方法,已经重载了 OnOutputDebugStringDebugEvent,从而将调试对象(前缀为 >)留下的踪迹重定向到专用的列表框中。还有可能将一个选择复制到剪贴板,或者搜索一个字符串,如图 12 所示。当利用 TRACE 或 OutputDebugString 添加跟踪时,它就会出现在该列表框中。这是一种调试代码的有效机制,并可以标出调试对象(前缀为 >)和调试器的输出结果之间的差异。 在另一个进程中插入运行代码 现在还有一个遗留问题需要解决:一定有一种方法可以使一些代码在另一个应用程序的上下文中运行。幸运的是,Jeffrey Richter 很早以前就在他的文章“Load Your 32-bit DLL into Another Process's Address Space Using INJLIB”(MSJ,1994 年 5 月)中解决了这个问题。 由于通常我们只对使用 GDI API 的应用程序感兴趣,因此我们可以假定这样的应用程序至少使用一个窗口来显示它的图形。(否则,它为何需要 GDI?)因此,在不同的解决方案中,基于 Windows 挂钩的方案好像是最佳的选择。当调用下面的挂钩时,任何进程中的任何线程执行 GetMessage 时 Windows 将会调用 GetMessageHookProc 回调函数: SetWindowsHookEx(WH_GETMESSAGE, GetMessageHookProc, hInstance, 0) 由于这是一个系统范围的挂钩(最后的参数为 0),回调函数的代码必须位于某个 DLL 中,该 DLL 被映射到其线程调用 GetMessage 的各个进程的地址空间。 如果挂钩进程和驱动应用程序适合,用预先定义的消息在它们之间建立通信信道就非常简单。这是一种允许调试器为一些插入代码(运行在被侦探应用程序上下文中)发送请求的不错方法,确切地说,这就是 GDI 对象显示对话框所需的!当挂钩进程拦截了第一条消息时,它首先重定向已经加载的 DLL 调用。然后,它启动一条新的线程,专门处理来自调试器的请求。这就在调试器和调试对象之间创建一条通信信道(有关详细信息,请参见 GDITrace.cpp 中的 StartInfiltratedThread)。 由调试器和调试对象从挂钩进程和渗透线程函数中调用的函数都已经收集到了 GDITrace.dll 中,GDITrace.dll 的行为由 CGDITraceApp 类来实现。该 DLL 与调试器应用程序 GDIUsage 静态链接,但是它动态加载到触发 Windows 挂钩的进程中。由调试器调用的函数在 _GdiTrace.h 中声明,并集合在 GdiTrace.cpp 中,从而帮助您理解调试器使用的是哪一部分,调试对象使用的是哪一部分。但为什么要在同一个 DLL 中混合不同的代码?需要在这两种代码之间共享一些变量,在同一个 DLL 的实例之间共享变量的值很简单,如图 13 所示。 这些代码用读/写/共享属性 (rws) 定义了名称为 .shared 的 PE 区域,这些属性包含 5 个前缀为 s_ 的需要共享的变量。根据这些声明,Windows 将这些变量保存到一个加载 DLL 的进程共享的内存块中。因此,这些变量在各个进程中的值都相同,特别是调试器和调试对象。我们看一下当调试器启动一个调试对象时会发生什么情况,以及如何使用这些变量。 当启动调试对象时,调试器线程接收到一个 CREATE_PROCESS_DEBUG_EVENT,它由 OnCreateProcessDebugEvent 处理,OnCreateProcessDebugEvent 反过来又调用 StartTraceGDI。该函数执行 SetSharedVariables,用调试器线程的 ID 设置 s_dwCallingThreadID 的值。如果当前进程的 ID 与保存在 s_dwProcessID 中的 ID 相同,挂钩进程就会知道它是在调试对象的上下文中运行,并根据已经加载的 DLL 开始修补 GDI 调用。接着,由挂钩进程启动的专用线程在 dwInfiltratedThreadID 中保存了它的 ID。最后,当该挂钩进程成功运行时,s_bDebuggeeIsStarted 被设置为 TRUE,然后由调试器用它来决定渗透线程是否已经准备好响应请求。 如果需要在调试器和调试对象之间传递或检索 GDI 对象句柄列表,就需要一个正好比一个 DWORD 或一个 BOOL 大的共享缓冲区。除了这 5 个变量,还要使用一个名为 GDITrace SharedBuffer 的内存映射文件,对应的内存由 CGDITraceApp 的成员 m_lpvMem 指定。它在 DLL 启动期间被初始化(有关详细的实现,请参见 GDITrace.cpp 中的 CGDITraceApp::InitInstance)。只有当这两个进程加载 DLL 时该缓冲区才需要创建和初始化:作为调试器的 GDIUsage 及其当前的调试对象。 s_dwProcessID 共享的变量用来识别两个进程间的区别。如果没有启动的调试对象,它的值总是 0;否则,它就包含调试对象进程的 ID。当 DLL 加载到进程中时,它的 InitInstance 检查 s_dwProcessID 是否等于 0(应当是 GDIUsage)或者等于 GetCurrentProcessId(应当是调试对象),从而创建内存映射文件。 从调试器到调试对象的通信 调试器使用 s_dwInfiltratedThreadID 共享变量来发送一个请求(利用 PostThreadMessage 通过一个简单的 Windows 消息),该请求将由调试对象中的渗透线程来处理。当调试对象通知调试器这样的一个请求已经完成时,需要另一个 s_dwCallingThreadID 共享变量。例如,当用户单击“Take Snapshot!”按钮时,GDIUsage 需要从调试对象收集已分配的 GDI 对象。 GDIUsage 发送一条 TM_GET_LIST 消息给调试对象中的渗透线程,调试对象的值保存在 s_dwInfiltratedThreadID 中。它将执行连同参数 UM_SNAPSHOT_READY 一起发送给 OnGetList 函数,该参数将被用作回调消息被 GDIUsage 主对话框接收。为什么不简单地使用同样的 TM_GET_LIST?答案与共享代码有关。“Take Snapshot!”与“Compare”按钮都需要获得相同的已分配 GDI 对象列表(虽然使用该列表的方法不同),以更新 GDIUsage 用户界面的相应部分。 为了总结一下在前面段落中我已经论述的内容,TM_GET_LIST 线程消息触发对调试对象端 GDI 对象的处理。另外,根据用户定义的两条消息,有两种方法可以更新 GDIUsage 主对话框:UM_SNAPSHOT_READY 与 UM_COMPARE_READY。 渗透线程唤醒并要求 OnGetList 来处理请求。该 CGDITraceApp 方法通过修补的存根枚举出调试对象中检测到的 GDI 分配,并将每个对象的说明(句柄值和类型)按照下面的 GDI_LIST 格式复制到由 m_lpvMem z指向的内存映射文件共享的缓冲区中: typedef struct { DWORD dwType; HGDIOBJ hObject; } GDI_ITEM; typedef struct { DWORD dwCount; // count of meaningful GDI_ITEM slots in Items GDI_ITEM Items[]; } GDI_LIST; 为了通知调试器列表已经准备好,一条相同的 TM_GET_LIST 消息被发送回由 s_dwCallingThreadID 识别的线程。该消息由负责调试事件的线程在 GDIUsage 上下文中接收并发送给 CGDIDebugger::OnThreadMessage。该方法通过用一个指向共享内存的指针向主对话框发送正确的用户消息(本示例为 UM_SNAPSHOT_READY)来通知 UI 线程,共享内存由内存映射文件定义,而调试对象将 GDI 对象保存在该文件中。通过使用 CGdiResources 的 CreateFromList 方法,这个自动封送处理的序列化列表用来实例化 CGdiResources。该类用来包装一个 GDI 对象列表,同时还提供诸如枚举与图形显示等服务。 死锁与计时问题 在深入了解远程 GDI 对象的图形显示之前,您应当了解一下可能发生的死锁问题。以前探讨的收集 GDI 对象的技术都是异步的,因为它要依赖调试器和调试对象之间交换的 Windows 消息。如果需要进行强同步通信,可以使用 Win32 事件。例如,渗透线程等待一个有特定名称的事件,当一条消息发送到它的队列中或者事件获得信号通知时,它就调用 MsgWaitForMultipleObjectsEx 来唤醒(有关详细的源代码信息,请参见 GDITrace.cpp 中的 CGDITraceApp::InfiltratedThreadProc)。在该实现中,用事件来要求线程结束它的生存期,因此不是真正的同步。 另一种需要真正同步行为的情况是,当调试对象加载一个 DLL 时修补 GDI 调用。为了截获 GDI 调用,调试器必须尽快通知渗透线程。否则,调试对象可能在安装截获存根之前就开始分配 GDI 对象。这里是一个不错的实现方案: 1. 调试器线程获得通知,通过 WaitForDebugEvent 返回的LOAD_DLL_DEBUG_EVENT 已在调试对象中加载了一个 DLL。 2. OnLoadDLLDebugEvent 重写方法接收 DLL 对应的 hModule,将它保存在一个新的共享变量中,并通过为事件发送信号来请求调试对象为该特定的 DLL 修补 GDI 调用。 3. 如果调试对象加载另一个 DLL,为了避免重新进入,OnLoadDLLDebugEvent 等待另一个在调试对象完成其修补工作后的信号通知事件。 4. MsgWaitForMultipleObjectsEx 唤醒调试对象-渗透线程,因为它等待的一个事件已经由信号通知。 5. CGDITraceApp::OnNewDLL 方法为在由共享变量定义的地址处加载的 DLL 重定向 GDI 调用,该共享变量由调试器用 DLL hModule 填充。 6. 调试器等待的事件由渗透线程发信号通知。 7. 渗透线程调用 MsgWaitForMultipleObjectsEx 等待完成另一个请求。 8. 调试器线程继续进行,因为它等待的事件已经由信号通知。 注意,最后两步的顺序号相同,因为它们的代码在由 Windows 调度的两个不同线程中运行。不要指望一个会在另一个之前执行。 虽然这种方案看起来很完美,但它会在调试器线程和插入调试对象的线程之间引起死锁。第 3 步和第 4 步之间有一个隐患。Win32 调试 API 假定调试器正在利用 WaitForDebugEventsumes 等待一个调试对象事件。当该函数返回时,一直到 ContinueDebugEvent 被调用,调试对象中的所有线程(甚至那些没有生成调试事件的线程)都被挂起。因此,停留在第 3 步和第 4 步的调试器线程将永远不会由调试对象执行,因为在同步通信结束后 ContinueDebugEvent 才应执行。要切记,从由调试器接收的调试事件来同步调试对象中的行为是不可能的。 在 GDIUsage 的情况下,基于消息的机制提供的通知速度好像已足够迅速。真正的问题出在其他地方。由于调试对象检索到第一条消息后就启动渗透线程,所以所有静态链接的 DLL 都已经初始化。如果其中的一些已经分配了 GDI 对象,这样的消耗将永远不会被 GDIUsage 检测到。在这种情况下,需要另一种方法来执行修补代码。GDIUsage 帮助您检测和找到的 GDI 对象创建不是在应用程序的生存期内发布的,而是在其启动时发布的。 显示 GDI 对象 图 1 中所示的 GDIUsage 用户界面允许用户获得在任何给定时间点某进程使用的 GDI 对象的快照,并且稍后可以与同一进程的当前状态进行对比。每组对象都保存在 CGdiResources 对象中。可以在 TM_GET_LIST 注释中看到,对象列表一旦通过内存映射文件在调试对象和调试器之间被序列化和封送处理,利用 CreateFromList 就可以初始化一个 CGdiResources 对象。 在 Windows 9x 版本的 GDIUsage 中,负责显示 GDI 对象的 CgdiResourcesDlg 类接受指向 CGdiResources 对象的指针作为参数。遗憾的是,在 Windows XP 版本的 GDIUsage 中,由于用来以图形方式显示 GDI 对象(在调试对象内部创建)的 GDI 函数总是出故障,因此在调试器的上下文中,CGdiResourcesDlg 就变得毫无价值。 解决方案是移去 GDITrace.dll 内部的 CGdiResourceDlg 和 CgdiResources,同先前讨论的一样,GDITrace.dll 通过 Windows 挂钩被加载到调试对象中。在检索完已分配对象的列表后,就该显示这个列表了。使用的通信机制和先前讨论的一样,但在显示列表并为渗透的线程发送消息之前,该列表必须在调试器的上下文中保存到内存映射文件。调试器将 CGdiResources 列表(或者当前的快照,或者对比的结果,取决于用户单击的按钮)序列化到内存映射文件,并将一条 TM_SHOW_LIST 消息发送给调试对象中渗透的线程,其中利用 GDIUsage 主对话框窗口的句柄作为参数。在这样的处理中,由 CGDIDebugger::ShowList 完成序列化,而由 ShowRemoteList 完成消息发送。 与 TM_GET_LIST 一样,由渗透的线程处理 TM_SHOW_LIST 消息,接着发送到 CGDITraceApp::OnShowList。这种方法根据内存映射文件中保存的序列化与封送数据初始化 CgdiResources。现在,有可能让 CGdiResourcesDlg 来显示对应的 GDI 对象了。 与 TM_GET_LIST 消息不同,为了将该显示命令发送给调试对象,调试器不需要任何返回代码或信息。在任何情况下,用户都希望 GDIUsage 保持禁用,直到他关闭了该 CGdiResourcesDlg 对话框。这就是将 GDIUsage 主对话框的句柄作为父窗口传递给 CGdiResourcesDlg 对象的原因。 除了有两点改进之外,该类的实现从 Windows 9x 版本以后就没有更改过。第一点改进是利用构建 GDI 句柄的方法来检测它是否引用一个常用对象以及是否在列表框的对应行中添加一个 *。这种功能在 GDIUsage 中是不可见的,因为创建常用对象的 API 没有被修补过,但您可能在编写自己的代码时想实现它。 第二点改进是,为了呈现对应的堆栈跟踪,用户单击或者双击 GDI 对象列表时需要进行检测,如图 10 所示。由于不应当将 CGdiResourcesDlg 代码链接到堆栈跟踪代码,因此定义了一个通用的回调接口 INotificationCallBack。CGDITraceApp 类派生于该界面,它实现了 OnDoubleClick 并使用 CGdiResourcesDlg::SetNotificationCallBack 对其自身进行注册。 当用户双击某个 GDI 对象时,CGdiResourcesDlg 要检查是否已经注册了一个回调。如果是,它就调用该回调的 OnDoubleClick 方法,并以被双击的 GDI 对象对应的 CGdiObj 说明作为参数。然后,CGDITraceApp 从传递的 CGdiObj 提取调用堆栈,并用它实例化 CcallStackDlg,从而为用户显示堆栈跟踪(有关实现的详细信息,请参见 GDITrace.cpp 中的 CGDITraceApp::OnDoubleClick)。 这种功能也添加到了图 3 所示的 GDIndicator 工具中。与 GDIUsage 不同,插入机制是基于 CreateRemoteThread 的,并且在我 8 月份的文章中已经进行过论述。一个要注意的有趣事实是,代码的序列化和显示与 GDIUsage 完全相同;只是更改了远程处理机制。由于没有被记录的堆栈跟踪,因此没有注册双击的处理程序。 还有最后两个缺陷必须进行处理。由于显示 GDI 对象的对话框陷入了另一个进程上下文,因此会发生一些怪异的事情。首先,在 Windows 2000 和 Windows XP 中,从另一个进程中设置前台的窗口绝非易事。您必须在拥有前台窗口的进程中调用 AllowSetForegroundWindow,从而让另一个进程设置它的一个窗口,并作为一个新的前台窗口。在用户取消之前,对话框代码会在另一个进程当前线程的上下文中无限循环地运行。因此,在关闭该对话框之前,GDIndicator 一直挂起。为了避免这种讨厌的行为,在对话框的生存期间,要隐藏它的主窗口。 第二个负面影响更难解决。对话框控制的线程中创建的窗口将不再接收它们的所有消息,因为该对话框进程对它们进行了筛选。例如,如果您使用这里概述的工具来找出由 Notepad 分配的 GDI 对象,它的重画功能将是不错的选择,而且可以轻松地导航到它的菜单中,但在关闭对话框之前,选择的命令不会触发事件。这是一种复杂的使用功能,但在搜索难以琢磨的 GDI 漏洞时确实颇有价值。 小结 Windows 9x 和 Windows XP 之间,GDI 已有所不同。虽然程序中的许多代码都重新使用了其 Windows 9x 实现中的代码,例如对 GDI 对象组的管理以及它们的图形显示,但获得由某个进程分配的 GDI 对象列表的内在机制却是全新的。Windows XP 版本是基于 Win32 调试 API、DLL 插入、Windows 挂钩和 API 补丁的,并且与对应的 Windows 9x 相比,提供的功能更多。 除了导致每个 GDI 分配的调用列表外,还增加了最后的处理。当远程进程终止时,插入的 DLL 的 ExitInstance 方法被调用。CGDITraceApp 最后利用该通知枚举 GDI 对象并转储仍然有效的对象。这与在调试版本中使用 DEBUG_NEW 作为内存分配器来检测内存泄漏时从 MFC 应用程序中获得的最后转储是一样的。
文章
存储  ·  API  ·  Windows
2017-10-09
有关Android插件化的一些总结思考
最近几年移动开发业界兴起了「 插件化技术 」的旋风,各个大厂都推出了自己的插件化框架,各种开源框架都评价自身功能优越性,令人目不暇接。随着公司业务快速发展,项目增多,开发资源却有限,如何能在有限资源内满足需求和项目的增长,同时又能快速响应问题和迭代新需求,这就是一个矛盾点。此时,插件化技术正好风生水起,去了解各个主流框架实现思路,看看能对目前工作是否有帮助,是很有必要的。 主要分为以下几个部分 插件化介绍入门知识实现原理主流框架实战小结插件化介绍 百度百科里是这么定义插件的:「 是一种遵循一定规范的应用程序接口编写出来的程序,只能运行在程序规定的系统平台下,而不能脱离指定的平台单独运行。」,也就是说,插件可以提供一种动态扩展能力,使得应用程序在运行时加载原本不属于该应用的功能,并且做到动态更新和替换。 那么在 Android 中,何为「 插件化 」,顾名思义,就是把一些核心复杂依赖度高的业务模块封装成独立的插件,然后根据不同业务需求进行不同组合,动态进行替换,可对插件进行管理、更新,后期对插件也可进行版本管理等操作。在插件化中有两个概念需要讲解下: 宿主所谓宿主,就是需要能提供运行环境,给资源调用提供上下文环境,一般也就是我们主 APK ,要运行的应用,它作为应用的主工程所在,实现了一套插件的加载和管理的框架,插件都是依托于宿主的APK而存在的。 插件插件可以想象成每个独立的功能模块封装为一个小的 APK ,可以通过在线配置和更新实现插件 APK 在宿主 APK 中的上线和下线,以及动态更新等功能。 那么为何要使用插件化技术,它有何优势,能给我们带来什么样好处,这里简单列举了以下几点: 让用户不用重新安装 APK 就能升级应用功能,减少发版本频率,增加用户体验。提供一种快速修复线上 BUG 和更新的能力。按需加载不同的模块,实现灵活的功能配置,减少服务器对旧版本接口兼容压力。模块化、解耦合、并行开发、 65535 问题。入门知识 首先我们要知道插件化技术是属于比较复杂一个领域,复杂点在于它涉及知识点广泛,不仅仅是上层做应用架构能力,还要求我们对 Android 系统底层知识需要有一定的认知,这里简单罗列了其中会涉及的知识点: 首先,要介绍的是 Binder ,我们都知道 Android 多进程通信核心就是 Binder ,如果没有它真的寸步难行。 Binder 涉及两层技术,你可以认为它是一个中介者模式,在客户端和服务器端之间, Binder 就起到中介的作用。如果要实现四大组件的插件化,就需要在 Binder 上做修改, Binder 服务端的内容没办法修改,只能改客户端的代码,而且四大组件的每个组件的客户端都不一样,这个就需要深入研究了。学习Binder的最好方式是 AIDL ,这方面在网上有很多资料,最简单的方式就是自己写个 aidl 文件自动生成一个 Java 类,然后去查看这个Java类的每个方法和变量,然后再去看四大组件,其实都是跟 AIDL 差不多的实现方式。 其次,是 App 打包的流程。代码写完了,执行一次打包操作,中途经历了资源打包、 Dex 生成、签名等过程。其中最重要的就是资源的打包,即 AAPT 这一步,如果宿主和插件的资源id冲突,一种解决办法就是在这里做修改。 第三, App 在手机上的安装流程也很重要。熟悉安装流程不仅对插件化有帮助,在遇到安装 Bug 的时候也非常重要。手机安装 App 的时候,经常会有下载异常,提示资源包不能解析,这时需要知道安装 App 的这段代码在什么地方,这只是第一步。第二步需要知道, App 下载到本地后,具体要做哪些事情。手机有些目录不能访问, App 下载到本地之后,放到哪个目录下,然后会生成哪些文件。插件化有个增量更新的概念,如何下载一个增量包,从本地具体哪个位置取出一个包,这个包的具体命名规则是什么,等等。这些细节都必须要清楚明白。 第四,是 App 的启动流程。 Activity 启动有几种方式?一种是写一个 startActivity ,第二种是点击手机 App ,通过手机系统里的 Launcher 机制,启动 App 里默认的 Activity 。通常, App 开发人员喜闻乐见的方式是第二种。那么第一种方式的启动原理是什么呢?另外,启动的时候,Main 函数在哪里?这个 Main 函数的位置很重要,我们可以对它所在的类做修改,从而实现插件化。 第五点更重要,做 Android 插件化需要控制两个地方。首先是插件 Dex 的加载,如何把插件 Dex 中的类加载到内存?另外是资源加载的问题。插件可能是 Apk 也可能是 so 格式,不管哪一种,都不会生成 R.id ,从而没办法使用。这个问题有好几种解决方案。一种是是重写 Context 的 getAsset 、 getResource 之类的方法,偷换概念,让插件读取插件里的资源,但缺点就是宿主和插件的资源 id 会冲突,需要重写 AAPT 。另一种是重写 AMS中保存的插件列表,从而让宿主和插件分别去加载各自的资源而不会冲突。第三种方法,就是打包后,执行一个脚本,修改生成包中资源id。 第六点,在实施插件化后,如何解决不同插件的开发人员的工作区问题。比如,插件1和插件2,需要分别下载哪些代码,如何独立运行?就像机票和火车票,如何只运行自己的插件,而不运行别人的插件?这是协同工作的问题。火车票和机票,这两个 Android 团队的各自工作区是不一样的,这时候就要用到 Gradle 脚本了,每个项目分别有各自的仓库,有各自不同的打包脚本,只需要把自己的插件跟宿主项目一起打包运行起来,而不用引入其他插件,还有更厉害的是,也可以把自己的插件当作一个 App 来打包并运行。 上面介绍了插件化的入门知识,一共六点,每一点都需要花大量时间去理解。否则,在面对插件化项目的时候,很多地方你会一头雾水。而只要理解了这六点核心,一切可迎刃而解。 实现原理 在Android中应用插件化技术,其实也就是动态加载的过程,分为以下几步: 把可执行文件( .so/dex/jar/apk 等)拷贝到应用 APP 内部。加载可执行文件,更换静态资源调用具体的方法执行业务逻辑Android 项目中,动态加载技术按照加载的可执行文件的不同大致可以分为两种: 动态加载 .so 库动态加载 dex/jar/apk文件(现在动态加载普遍说的是这种)第一点, Android 中 NDK 中其实就使用了动态加载,动态加载 .so 库并通过 JNI 调用其封装好的方法。后者一般是由 C/C++ 编译而成,运行在 Native 层,效率会比执行在虚拟机层的 Java 代码高很多,所以 Android 中经常通过动态加载 .so 库来完成一些对性能比较有需求的工作(比如 Bitmap 的解码、图片高斯模糊处理等)。此外,由于 .so 库是由 C/C++ 编译而来的,只能被反编译成汇编代码,相比中 dex 文件反编译得到的 Smali 代码更难被破解,因此 .so 库也可以被用于安全领域。 其二,“基于 ClassLoader 的动态加载 dex/jar/apk 文件”,就是我们指在 Android 中 动态加载由 Java 代码编译而来的 dex 包并执行其中的代码逻辑,这是常规 Android 开发比较少用到的一种技术,目前说的动态加载指的就是这种。 Android 项目中,所有 Java 代码都会被编译成 dex 文件,Android 应用运行时,就是通过执行 dex 文件里的业务代码逻辑来工作的。使用动态加载技术可以在 Android 应用运行时加载外部的 dex 文件,而通过网络下载新的 dex 文件并替换原有的 dex 文件就可以达到不安装新 APK 文件就升级应用(改变代码逻辑)的目的。 所以说,在 Android 中的 ClassLoader 机制主要用来加载 dex 文件,系统提供了两个 API 可供选择: PathClassLoader:只能加载已经安装到 Android 系统中的 APK 文件。因此不符合插件化的需求,不作考虑。DexClassLoader:支持加载外部的 APK、Jar 或者 dex 文件,正好符合文件化的需求,所有的插件化方案都是使用 DexClassloader 来加载插件 APK 中的 .class文件的。主流框架 在 Android 中实现插件化框架,需要解决的问题主要如下: 资源和代码的加载Android 生命周期的管理和组件的注册宿主 APK 和插件 APK 资源引用的冲突解决下面分析几个目前主流的开源框架,看看每个框架具体实现思路和优缺点。 DL 动态加载框架 ( 2014 年底) 是基于代理的方式实现插件框架,对 App 的表层做了处理,通过在 Manifest 中注册代理组件,当启动插件组件时,首先启动一个代理组件,然后通过这个代理组件来构建,启动插件组件。 需要按照一定的规则来开发插件 APK,插件中的组件需要实现经过改造后的 Activity、FragmentActivity、Service 等的子类。 优点如下: 插件需要遵循一定的规则,因此安全方面可控制。方案简单,适用于自身少量代码的插件化改造。缺点如下: 不支持通过 This 调用组件的方法,需要通过 that 去调用。由于 APK 中的 Activity 没有注册,不支持隐式调用 APK 内部的 Activity。插件编写和改造过程中,需要考虑兼容性问题比较多,联调起来会比较费时费力。DroidPlugin ( 2015 年 8 月) DroidPlugin 是 360 手机助手实现的一种插件化框架,它可以直接运行第三方的独立 APK 文件,完全不需要对 APK 进行修改或安装。一种新的插件机制,一种免安装的运行机制,是一个沙箱(但是不完全的沙箱。就是对于使用者来说,并不知道他会把 apk 怎么样), 是模块化的基础。 实现原理: 共享进程:为android提供一个进程运行多个 apk 的机制,通过 API 欺骗机制瞒过系统。占坑:通过预先占坑的方式实现不用在 manifest 注册,通过一带多的方式实现服务管理。Hook 机制:动态代理实现函数 hook ,Binder 代理绕过部分系统服务限制,IO 重定向(先获取原始 Object –> Read ,然后动态代理 Hook Object 后–> Write 回去,达到瞒天过海的目的)。插件 Host 的程序架构: 优点如下: 支持 Android 四大组件,而且插件中的组件不需要在宿主 APK 中注册。支持 Android 2.3 及以上系统,支持所有的系统 API。插件与插件之间,插件与宿主之间的代码和资源完全隔阂。实现了进程管理,插件的空进程会被及时回收,占用内存低。缺点如下: 插件 APK 中不支持自定义资源的 Notification,通知栏限制。插件 APK 中无法注册具有特殊的 IntentFilter 的四大组件。缺乏对 Native 层的 Hook 操作,对于某些带有 Native 代码的插件 APK 支持不友好,可能无法正常运行。由于插件与插件,插件与宿主之间的代码完全隔离,因此,插件与插件,插件与宿主之间的通信只能通过 Android 系统级别的通信方式。安全性担忧(可以修改,hook一些重要信息)。机型适配(不是所有机器上都能行,因为大量用反射相关,如果rom厂商深度定制了framework层,反射的方法或者类不在,容易插件运用失败)Small ( 2015 年底) Small 是一种实现轻巧的跨平台插件化框架,基于“轻量、透明、极小化、跨平台”的理念,实现原理有以下三点。 动态加载类:我们知道插件化很多都从 DexClassLoader 类有个 DexPathList 清单,支持 dex/jar/zip/apk 文件格式,却没有支持 .so 文件格式,因此 Small 框架则是把 .so 文件包装成 zip 文件格式,插入到 DexPathList 集合中,改写动态加载的代码。资源分段:由于 Android 资源的格式是 0xPPTTNNNN ,PP 是包 ID ,00-02 是属于系统,7f 属于应用程序,03-7e 则保留,可以在这个范围内做文章 , TT 则是 Type 比如,attr 、layout 、string 等等,NNNN 则是资源全局 ID。那么这个框架则是对资源包进行重新打包,每个插件重新分配资源 ID ,这样就保证了宿主和插件的资源不冲突。动态代理注册:在 Android 中要使用四大组件,都是需要在 manifest 清单中注册,这样才可以使用,那如何在不注册情况也能使用呢,这里就是用到动态代理机制进行 Hook ,在发送 AMS 之前用占坑的组件来欺骗系统,通过认证后,再把真正要调用的组件还原回来,达到瞒天过海目的。架构图: 优点如下: 所有插件支持内置宿主包中。插件的编码和资源文件的使用与普通开发应用没有差别。通过设定 URI ,宿主以及 Native 应用插件,Web 插件,在线网页等能够方便进行通信。支持 Android 、 iOS 、和 Html5 ,三者可以通过同一套 Javascript 接口实现通信。缺点如下: 暂不支持 Service 的动态注册,不过这个可以通过将 Service 预先注册在宿主的 AndroidManifest.xml 文件中进行规避,因为 Service 的更新频率通常非常低。与其他主流框架的区别: DyLA : Dynamic-load-apk @singwhatiwanna DiLA : Direct-Load-apk @FinalLody APF : Android-Plugin-Framework @limpoxe ACDD : ACDD @bunnyblue DyAPK : DynamicAPK @TediWang DPG : DroidPlugin @cmzy, 360 功能 透明度 VirtualAPK 是滴滴开源的一套插件化框架,支持几乎所有的 Android 特性,四大组件方面。 架构图: 实现思路: VirtualAPK 对插件没有额外的约束,原生的 apk 即可作为插件。插件工程编译生成 apk后,即可通过宿主 App 加载,每个插件 apk 被加载后,都会在宿主中创建一个单独的 LoadedPlugin 对象。如下图所示,通过这些 LoadedPlugin 对象,VirtualAPK 就可以管理插件并赋予插件新的意义,使其可以像手机中安装过的 App 一样运行。 合并宿主和插件的ClassLoader 需要注意的是,插件中的类不可以和宿主重复合并插件和宿主的资源 重设插件资源的 packageId,将插件资源和宿主资源合并去除插件包对宿主的引用 构建时通过 Gradle 插件去除插件对宿主的代码以及资源的引用 特性如下: 四大组件均不需要在宿主manifest中预注册,每个组件都有完整的生命周期。 Activity:支持显示和隐式调用,支持Activity的theme和LaunchMode,支持透明主题; Service:支持显示和隐式调用,支持Service的start、stop、bind和unbind,并支持跨进程bind插件中的Service; Receiver:支持静态注册和动态注册的Receiver; ContentProvider:支持provider的所有操作,包括CRUD和call方法等,支持跨进程访问插件中的Provider。 自定义View:支持自定义 View,支持自定义属性和style,支持动画; PendingIntent:支持PendingIntent以及和其相关的Alarm、Notification和AppWidget; 支持插件Application以及插件manifest中的meta-data; 支持插件中的so。 优秀的兼容性 兼容市面上几乎所有的Android手机,这一点已经在滴滴出行客户端中得到验证。资源方面适配小米、Vivo、Nubia 等,对未知机型采用自适应适配方案。极少的 Binder Hook,目前仅仅 hook了两个Binder:AMS和IContentProvider,hook 过程做了充分的兼容性适配。插件运行逻辑和宿主隔离,确保框架的任何问题都不会影响宿主的正常运行。入侵性极低 插件开发等同于原生开发,四大组件无需继承特定的基类;精简的插件包,插件可以依赖宿主中的代码和资源,也可以不依赖;插件的构建过程简单,通过 Gradle 插件来完成插件的构建,整个过程对开发者透明。如下是 VirtualAPK 和主流的插件化框架之间的对比。 ![image](https://yqfile.alicdn.com/6c28aa88cf2cd04d58c5b0534e2f98e53a79615e.png) RePlugin是一套完整的、稳定的、适合全面使用的,占坑类插件化方案,由360手机卫士的RePlugin Team研发,也是业内首个提出”全面插件化“(全面特性、全面兼容、全面使用)的方案。 框架图: 主要优势有:极其灵活:主程序无需升级(无需在Manifest中预埋组件),即可支持新增的四大组件,甚至全新的插件非常稳定:Hook 点仅有一处(ClassLoader),无任何 Binder Hook!如此可做到其崩溃率仅为“万分之一”,并完美兼容市面上近乎所有的 Android ROM。特性丰富:支持近乎所有在“单品”开发时的特性。包括静态 Receiver、 Task-Affinity 坑位、自定义 Theme、进程坑位、AppCompat、DataBinding等。易于集成:无论插件还是主程序,只需“数行”就能完成接入。管理成熟:拥有成熟稳定的“插件管理方案”,支持插件安装、升级、卸载、版本管理,甚至包括进程通讯、协议版本、安全校验等。数亿支撑:有 360 手机卫士庞大的数亿用户做支撑,三年多的残酷验证,确保App用到的方案是最稳定、最适合使用的。 实战 主要是测试各个框架之间上手的容易度如何,并做不同对比,这边写了两个 Demo 例子,一个是基于 Small 框架,一个基于 VirtualAPK 框架,从中能看出不同。 Small 实践 要引用官方最新的版本,不然在宿主和插件合并build.gradle 的时候会出现一个 BUG,这是个坑位,注意行走。其次在模块命名上要遵循一定的规则,比如业务模块用 app. ,公共库模块用 lib. ,相当于包名 .app.,.lib. 。每次在插件中添加一个 activity 组件,都需要在宿主中配置路由,然后在重新编译插件一遍,不然直接运行的话,在宿主中是找到新添加的 activity 组件,会报该组件没在系统 manifest 中,所以每次新增或修改建议插件都重新编译一遍。官方里说了,对于 Service 支持不太友好,就没去实践了。 VirtualAPK 实践有个坑需要注意的是构建环境,官方说明是要以下版本环境,Gradle 2.14.1 和 com.android.tools.build 2.1.3, 之前编译的是用最新的Gradle版本,导致一直有问题,至于是否有其他问题,可以看官方文档。 具体代码Small Demo :https://github.com/cr330326/MySmall VirtualAPK Demo :https://github.com/cr330326/MyVirtualAPKDemo 小结 正如开头所说,要实现插件化的框架,无非就是解决那典型的三个问题:插件代码如何加载、插件中的组件生命周期如何管理、插件资源和宿主资源冲突怎么办。每个框架针对这三个问题,都有不同的解决方案,同时呢,根据时间顺序,后出来的框架往往都会吸收已经出的框架精髓,进而修复那些比较有里程碑意义框架的不足。但这些框架的核心思想都是用到了代理模式,有的在表面层进行代理,有的则在系统应用层进行代理,通过代理达到替换和瞒天过海,最终让 Android 系统误以为调用插件功能和调用原生开发的功能是一样的,进而达到插件化和原生兼容编程的目的。 原文发布时间为:2018-07-19本文作者:斜杠Allen本文来自云栖社区合作伙伴“安卓巴士Android开发者门户”,了解相关信息可以关注“安卓巴士Android开发者门户”。
文章
安全  ·  Java  ·  API  ·  Android开发  ·  开发者
2018-07-20
如何制作外挂
一、先说一下写一个外挂需要什么条件 1、熟练的C语言知识 目前的外挂大部分都是用BC或者是vc写的,拥有熟练的C语言知识是写外挂的基本条件 2、具有很强的汇编基础 一般游戏都不可能有原代码的,必须靠反汇编或者跟踪的办法来探索其中的机理 ,所以有强的汇编基础也是必不可少的条件 3、熟练掌握跟踪和调试的工具 有了上面2个条件后,掌握一些工具也是很有必要的 跟踪的工具,softice当然是不二之选,至于反汇编的工具,我推荐用IDA PRO 这个工具反汇编出来的代码结构清晰,非常好读 如果你不具有上面的条件,还是先把基础打好,再来写外挂吧,一分耕耘,一分收获,天下没有白掉的馅饼的 二、写外挂面临的基本技术问题 1、修改进程的执行代码 要修改进程的执行代码,要先取得进程的ID,如果是由外挂程序启动,返回值里就有进程ID,如果不是的话, 需要用findwindow找到窗口句柄,再用GetWindowProcessID取得进程ID,取得进程ID以后,就可以用 writeprocessmemory来修改进程的执行代码了,使程序按照我们的意愿来执行,石器外挂里的不遇敌、寸步遇敌 就是用这样的方法来实现的 2、截获外挂发送和接收的封包 除了通过修改代码来实现的功能以外,很多的功能都是通过修改封包来实现的,要修改封包,首先要能截获它。 第一步是要跟踪出发和收的位置,至于怎么跟踪,我以后会提到,找到位置以后,有2个办法,一是在那个位置加一 个jmp语句,跳到你的处理函数位置,处理完后,再跳回来,这种方法要求比较高,需要处理好很多事情,另一种办法 是往那个位置写条能造成例外的指令,比如int 3,然后用DebugActiveProcess调试游戏进程,这样每当游戏执行到那个 位置的时候,就会停下来,到外挂程序里面去,等外挂程序处理完以后,用ContinueDebugEvent 继续运行程序。 今天先写这么多,下回将讨论外挂的具体功能该怎么实现 今天来谈谈地址的调查问题,地址调查是写外挂中最艰辛,最富有挑战性的事情,很多朋友问我要外挂的原程序,其实有了外挂原程序,如果你不会调查地址,还是没用的, 原程序和地址的关系就象武学中招式与内功的关系,没有内功的招式,只是一个花架子。而内功精深以后,任何普通的招式,都有可能化腐朽为神奇,外挂中的地址分为两类,一类是程序地址,一类是数据地址。象石器中的双石器,真彩,不遇敌,寸步遇敌,发送接收封包等,都属于第一类,而人物坐标,状态等,都属于第二类。对于第一类地址,主要依靠softice来调查地址,对第二类地址,可以用一些游戏工具,比如fpe,game expert,game master等来调查,我一直用game expert,因为我找不到2000下能用的fpe, 各位以前用fpe改游戏的时候,没想过他也能用来干这个吧 对于第二类数据的调查方法,大部分人都很熟习了,我就不多说了,现在主要来谈谈第一类数据的详细调查过程,比如我们要调查发送封包的位置,如何着手呢,客户端往服务器要发很多封包,但最简单的办法莫过从说话的封包入手,先说一句很长的话,最好是英文,查起来方便,说完以后,用任意一种办法进入游戏程序的进程空间(比如先用spy查出游戏程序的窗口句柄,再切换到softice打入bmsg 窗口句柄 wm_lbuttondown,这样在游戏程序中一点鼠标就进入了他的进程空间)然后用s命令查出这句话所放的内存地址,记下这个地址,在softice 中打入bpm 刚才调查到的地址,这个指令的意思是只要有访问这个内存的动作,立刻中断,然后再切换到游戏,说一句话,你会发现softice自动中断到某一个位置了,从这个位置跟踪下去,发送封包的位置也就不远了。 上面所说的都是针对一个全新的游戏程序而言,如果是一个老的程序,有前辈做了大量的工作,还可以用些别的办法,如反汇编等,来调查。以后游戏版本的更新也是如此,只要把老版本的地址位置附近的代码记下来,去新版本的代码里面search一下,就ok了。 恩,休息一会儿,休息一会儿 我主要对外挂的技术进行分析,至于游戏里面的内部结构每个都不一样,这里就不做讲解了,我也没有那么厉害,所有的都知道,呵呵!1 首先游戏外挂的原理外挂现在分为好多种,比如模拟键盘的,鼠标的,修改数据包的,还有修改本地内存的,但好像没有修改服务器内存的哦,呵呵!其实修改服务器也是有办法的,只是技术太高一般人没有办法入手而已!(比如请GM去夜总会,送礼,收黑钱等等办法都可以修改服务器数据,哈哈)修改游戏无非是修改一下本地内存的数据,或者截获api函数等等,这里我把所能想到的方法都作一个介绍,希望大家能做出很好的外挂来使游戏厂商更好的完善自己的技术.我见到一片文章是讲魔力宝贝的理论分析,写的不错,大概是那个样子.下来我就讲解一下技术方面的东西,以作引玉之用2 技术分析部分1 模拟键盘或鼠标的响应我们一般使用UINT SendInput(UINT nInputs, // count of input eventsLPINPUT pInputs, // array of input eventsint cbSize // size of structure);api函数第一个参数是说明第二个参数的矩阵的维数的,第二个参数包含了响应事件,这个自己填充就可以,最后是这个结构的大小,非常简单,这是最简单的方法模拟键盘鼠标了,呵呵注意:这个函数还有个替代函数: VOID keybd_event(BYTE bVk, // 虚拟键码BYTE bScan, // 扫描码DWORD dwFlags, ULONG_PTR dwExtraInfo // 附加键状态);和VOID mouse_event(DWORD dwFlags, // motion and click optionsDWORD dx, // horizontal position or changeDWORD dy, // vertical position or changeDWORD dwData, // wheel movementULONG_PTR dwExtraInfo // application-defined information);这两个函数非常简单了,我想那些按键精灵就是用的这个吧,呵呵,上面的是模拟键盘,下面的是模拟鼠标的.这个仅仅是模拟部分,要和游戏联系起来我们还需要找到游戏的窗口才行,或者包含快捷键,就象按键精灵的那个激活键一样,我们可以用GetWindow函数来枚举窗口,也可以用Findwindow函数来查找制定的窗口(注意还有一个FindWindowEx),FindwindowEx可以找到窗口的子窗口,比如按钮,等什么东西.当游戏切换场景的时候我们可以用FindWindowEx来确定一些当前窗口的特征,从而判断是否还在这个场景,方法很多了, 比如可以GetWindowInfo来确定一些东西,比如当查找不到某个按钮的时候就说明游戏场景已经切换了,等等办法.有的游戏没有控件在里面,这是对图像做坐标变换的话,这种方法就要受到限制了.这就需要我们用别的办法来辅助分析了.至于快捷键我们要用动态连接库实现了,里面要用到hook技术了,这个也非常简单,大家可能都会了,其实就是一个全局的hook对象然后SetWindowHook就可以了,回调函数都是现成的,而且现在网上的例子多如牛毛,这个实现在外挂中已经很普遍了.如果还有谁不明白,那就去看看msdn查找SetWindowHook就可以了.这个动态连接库的作用很大,不要低估了哦,它可以切入所有的进程空间,也就是可以加载到所有的游戏里面哦,只要用对,你会发现很有用途的!这个需要你复习一下win32编程的基础知识了,呵呵,赶快去看书吧!2截获消息有些游戏的响应机制比较简单,是基于消息的,或者用什么定时器的东西,这个时候你就可以用拦截消息来实现一些有趣的功能了.我们拦截消息使用的也是hook技术,里面包括了键盘消息,鼠标消息,系统消息,日志等,别的对我们没有什么大的用处,我们只用拦截消息的回调函数就可以了,这个不会让我写例子吧,其实这个和上面的一样,都是用SetWindowHook来写的,看看就明白了很简单的.至于拦截了以后做什么就是你的事情了,比如在每个定时器消息里面处理一些我们的数据判断,或者在定时器里面在模拟一次定时器,那么有些数据就会处理两次,呵呵,后果嘛,不一定是好事情哦,呵呵,不过如果数据计算放在客户端的游戏就可以真的改变数据了,呵呵,试试看吧!用途还有很多,自己想也可以想出来的,呵呵!3拦截socket包这个技术难度要比原来的高很多哦,要有思想准备.首先我们要替换winSock.dll或者 winsock32.dll,我们写的替换函数要和原来的函数一致才行,就是说它的函数输出什么样的,我们也要输出什么样子的函数,而且参数,参数顺序都要一样才行,然后在我们的函数里面调用真正的winSock32.dll里面的函数就可以了首先:我们可以替换动态库到系统路径其次:我们应用程序启动的时候可以加载原有的动态库,用这个函数LoadLibary然后定位函数入口用GetProcAddress函数获得每个真正socket函数的入口地址当游戏进行的时候它会调用我们的动态库,然后从我们的动态库中处理完毕后才跳转到真正动态库的函数地址,这样我们就可以在里面处理自己的数据了,应该是一切数据.呵呵!兴奋吧,拦截了数据包我们还要分析之后才能进行正确的应答,不要以为这样工作就完成了,呵呵!还早呢,等分析完毕以后我们还要仿真应答机制来和服务器通信,一个不小心就会被封号,呵呵,呜~~~~~~~~我就被封了好多啊!分析数据才是工作量的来源呢,游戏每次升级有可能加密方式会有所改变,因此我们写外挂的人都是亡命之徒啊,被人娱乐了还不知道,呵呵!(声明我可没有赚钱,我是免费的)好了,给大家一个不错的起点,这里有完整的替换socket源代码,呵呵!http://www.vchelp.net/vchelp/zsrc/wsock32_sub.zip 4截获api上面的技术如果可以灵活运用的话我们就不用截获api函数了,其实这种技术是一种补充技术.比如我们需要截获socket以外的函数作为我们的用途,我们就要用这个技术了,其实我们也可以用它直接拦截在socket中的函数,这样更直接.现在拦截api的教程到处都是,我就不列举了,我用的比较习惯的方法是根据输入节进行拦截的,这个方法可以用到任何一种操作系统上,比如98/2000等, 有些方法不是跨平台的,我不建议使用.这个技术大家可以参考windows核心编程里面的545页开始的内容来学习,如果是98系统可以用window系统奥秘那个最后一章来学习.好了方法就是这么多了,看大家怎么运用了,其它的一些针对性的技巧这里我就不说了,要不然会有人杀了我的,呵呵!记住每个游戏的修改方法都不一样,如果某个游戏数据处理全部在服务器端,那么你还是别写外挂了,呵呵,最多写个自动走路的外挂,哈哈!数据分析的时候大家一定要注意,不要轻易尝试和服务器的连接,因为那有很危险,切忌!等你掌握了大量的数据分析结果以后,比较有把握了在试试,看看你的运气好不好,很有可能会成功的哦,呵呵!其实像网金也疯狂的那种模拟客户端的程序也是不错的,很适合office的人用,就看大家产品定位了.好了不说了,大家努力吧!切忌不要被游戏厂商招安哦,那样有损我们的形象,我们是为了让游戏做的更好而开发的,也不愿意打乱游戏的平衡,哎,好像现在不是这样了!不了随其自然吧!
文章
API
2014-10-17
高德地图驾车导航内存优化原理与实战
​背景 根据Apple官方WWDC的回答,减少内存可以让用户体验到更快的启动速度,不会因为内存过大而导致Crash,可以让APP存活的更久。 对于高德地图来说,根据线上数据的分析,内存过高会导致导航过程中系统强杀OOM。尤其区别于其他APP的地方是,一般APP只需要关注前台内存过高的系统强杀FOOM,高德地图有不少用户使用后台导航,所以也需要关注后台的内存过高导致的系统强杀BOOM,且后台强杀较前台强杀更为严重。为了提升用户体验,内存治理迫在眉睫。 原理剖析 OOM OOM是Out of Memory的缩写。在iOS APP中如果内存超了,系统会把APP直接杀死,一种另类的Crash,且无法捕获。发现OOM时,我们可以从设备->隐私->分析与改进->分析数据中找到以JetsamEvent开头的日志,日志里面记录了很多信息:手机设备信息、系统版本、内存大小、CPU时间等。 Jetsam Jetsam是iOS系统的一种资源管理机制。不同于MacOS、Linux、Windows等,iOS中没有内存交换空间,所以在设备整体内存紧张时,系统会将一些优先级不高或者占用内存过大的直接Kill掉。 通过iOS开源的XNU内核源码可以分析到: 每个进程在内核中都存在一个优先级列表,JetSam在受到内存压力时会从优先级列表最低的进程开始尝试杀死,直到内存水位恢复到正常水位。 Jetsam是通过get_task_phys_footprint获取到phys_footprint的值,来决定要不要杀掉应用。 Jetsam机制清理策略可以总结为以下几点: 单个APP物理内存占用超过上限会被清理,不同的设备内存水位线不一样。 整个设备物理内存占用受到压力时,优先清理后台应用,再清理前台应用。 优先清理内存占用高的应用,再内存占用低的应用。 相比系统应用,会优先清理用户应用。 Android端为Low Memory Killer: 根据APP的优先级和使用总内存的多少,系统会在设备内存吃紧情况下强杀应用。 内存吃紧的判断取决于系统RSS(实际使用物理内存,包含共享库占用的全部内存)的大小。 关键参数有3个: 1)oom_adj:在Framework层使用,代表进程的优先级,数值越高,优先级越低,越容易被杀死。 2)oom_adj threshold:在Framework层使用,代表oom_adj的内存阈值。Android Kernel会定时检测当前剩余内存是否低于这个阀值,若低于则杀死oom_adj ≥该阈值对应的oom_adj中,数值最大的进程,直到剩余内存恢复至高于该阀值的状态。 3)oom_score_adj:在Kernel层使用,由oom_adj换算而来,是杀死进程时实际使用的参数。 数据分析 phys_footprint获取iOS应用总的物理内存,具体可以参考官方说明iOS Memory Deep Dive. std::optional<size_t> memoryFootprint() { task\_vm\_info\_data\_t vmInfo; mach\_msg\_type\_number\_t count = TASK\_VM\_INFO_COUNT; kern\_return\_t result = task\_info(mach\_task\_self(), TASK\_VM\_INFO, (task\_info_t) &vmInfo, &count); if (result != KERN_SUCCESS) return std::nullopt; return static\_cast<size\_t>(vmInfo.phys_footprint); } Instruments-VM Tracker可以用来分析具体内存分类,比如Malloc部分是堆内存,Webkit Malloc部分是JavaScriptCore占用的内存等。需要注意的是每个分类的内存值 = Dirty Size + Swapped。 通过Instruments VM Tracker抓取导航中内存分布进行对比分析。导航前台静置时,高德地图的总内存数值非常高,其中IOKit、WebKit Malloc和Malloc堆内存为内存占用大头。 在分析过程中可以使用的工具很多,各有优缺点,需要配合使用,相互弥补。我们在分析的过程中主要用到Intruments VM Tracker、Allocations、Capture GPU Frame、MemGraph、dumpsys meminfo 、Graphics API Debugger、Arm Mobile Studio、AJX 内存分析工具、自研Malloc分析工具等。 IOKit内存为地图渲染显存部分。 WebKit Malloc内存为AJX JS业务内存。 Malloc堆内存,我们通过Hook Malloc分配内存的API,通过抓取堆栈分析具体内存消费者。 治理优化 根据上面的数据分析,很容易做出从大头开始抓起的思路。我们在治理过程中的大体思路: 分析数据:从内存大头开始,分析各内存归属业务,以便业务进一步分析优化。 内存治理:优化技术方案减少内存开销、高低端机功能分级和智能容灾(即内存告警时通过功能降级等策略释放内存)。 分而治之 据数据分析,高德地图三大内存消耗分别是地图渲染(Graphic显存)、功能业务(JavaScriptCore)和通用业务(Malloc)。我们也主要从这三个方面入手优化。 地图Graphic显存优化 Xcode自带Debug工具Capture GPU Frame,可以分析出具体显存占用,显存主要分为纹理Texture部分和Buffer部分,通过详细的地址信息分析具体消耗。Android端类似分析显存工具可以用Google的Graphics API Debugger。 根据分析,Texture部分我们通过FBO绘制方式调整、矢量路口大图背景优化、图标跨页面释放、文字纹理优化、低端机关闭全屏抗锯齿等减少显存消耗。Buffer部分通过开启低显存模式、关闭四叉树预加载、切后台释放缓存资源等。 Webkit Malloc优化 高德地图使用的是自研的动态化方案,依赖于iOS系统提供的框架JavaScriptCore,使用的业务内存消耗大多会被系统归类到WebKit Malloc,从系统工具Instruments上的VM Tracker可以看出。此处有两个思路,一个是业务自身优化内存消耗,第二个是动态化引擎和框架优化内存消耗。 业务自身优化,动态化方案的IDE提供内存分析工具可以清晰的输出具体业务内存消耗在什么地方,便于业务同学分析是否合理。 动态化引擎和框架优化,我们通过优化对系统库JavaScriptCore的使用方式,即多个JSContextRef上下文共享同一份JSContextGroupRef的方式。多个页面可以共享一份框架代码,从而减少内存开销。 Malloc堆内存优化 iOS端堆内存分配基本上使用的libmalloc库,其中包含以下几个内存操作接口: // c分配方法 void *malloc(size\_t \_\_size) \_\_result\_use\_check \_\_alloc_size(1); void *calloc(size\_t \_\_count, size\_t \_\_size) \_\_result\_use\_check \_\_alloc_size(1,2); void free(void *); void \*realloc(void \*\_\_ptr, size\_t \_\_size) \_\_result\_use\_check \_\_alloc\_size(2); void *valloc(size\_t) \_\_alloc_size(1); // block分配方法 // Create a heap based copy of a Block or simply add a reference to an existing one. // This must be paired with Block_release to recover memory, even when running // under Objective-C Garbage Collection. BLOCK\_EXPORT void \*\_Block_copy(const void \*aBlock) \_\_OSX\_AVAILABLE\_STARTING(\_\_MAC\_10\_6, \_\_IPHONE\_3_2); 通过hook内存操作API记录下内存分配的堆栈、大小,即可分析内存使用情况。 同时源码中还存在一个全局钩子函数malloc_logger ,可输出Malloc过程中的日志,定义如下: // We set malloc_logger to NULL to disable logging, if we encounter errors // during file writing typedef void(malloc\_logger\_t)(uint32_t type, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3, uintptr_t result, uint32\_t num\_hot\_frames\_to_skip); extern malloc\_logger\_t *malloc_logger; iOS堆内存分析方案,可通过hook malloc系列API,也可以设置malloc_logger的函数实现,即可记录下堆内存使用情况。 此方案有几个难点问题,每秒钟内存分配的量级大、内存有分配有释放需要高效查询和堆栈反解聚合。为此我们设计了一套完整的Malloc堆内存分析方案,来满足快速定位堆内存归属,以便分发到各自业务Owner分析优化。 统一管理 随着业务的增长给高德地图这个超级APP带来了极大资源压力,因此我们沉淀了一套自适应资源管理框架,来满足不同业务场景在有限资源下能够做到功能和体验极致均衡。主要的设计思路是通过监测用户设备等级、系统状态、当前业务场景以及用户行为,利用调度算法进行实时推算,统一管理协调APP当前资源状态分配,对用户当前不可见的内存等资源进行回收。 自适应资源管理框架-内存部分 可以根据不同的设备等级、业务场景、用户行为和系统状态来管理资源。各业务都可以很容易的接入此框架,目前已经应用到多个业务场景,均有不错的收益。 数据验收 通过三个版本的连续治理,前后台导航场景均有50%的收益,同时Abort率也有10%~20%的收益。整体收益算是比较乐观,但是随之而来的挑战是我们该如何守住成果。 长线管控 所谓打江山容易守江山难,如果没有长线管控的方案,随着业务的版本迭代,不出三五个版本就会将先前的优化消耗。为此我们构建了一套APM性能监控平台,在研发测试阶段发现并解决问题,不把问题带上线。 APM性能监控平台 为了将APP的性能做到日常监控,我们建设了一套线下「APM性能监控平台」,平台能够支持常规业务场景的性能监控,包括:内存、CPU、流量等,能够及时的发现问题并进行报警。再配合性能跟进流程,为客户端性能保障把好最后一关。 内存分析工具 Xcode memory gauge:在Xcode的Debug navigator中,可以粗略查看内存占用的情况。 Instruments - Allocations:可以查看虚拟内存占用、堆信息、对象信息、调用栈信息、VM Regions信息等。可以利用这个工具分析内存,并针对地进行优化。 Instruments - Leaks:用于检测内存泄漏。 Instruments - VM Tracker:可以查看内存占用信息,查看各类型内存的占用情况,比如dirty memory的大小等等,可以辅助分析内存过大、内存泄漏等原因。 Instruments - Virtual Memory Trace:有内存分页的具体信息,具体可以参考WWDC 2016 - Syetem Trace in Depth。 Memory Resource Exceptions:从Xcode 10开始,内存占用过大时,调试器能捕获到EXC_RESOURCE RESOURCE_TYPE_MEMORY异常,并断点在触发异常抛出的地方。 Xcode Memory Debugger:Xcode中可以直接查看所有对象间的相互依赖关系,可以非常方便的查找循环引用的问题。同时,还可以将这些信息导出为memgraph文件。 memgraph + 命令行指令:结合上一步输出的memgraph文件,可以通过一些指令来分析内存情况。vmmap可以打印出进程信息,以及VMRegions的信息等,结合grep可以查看指定VMRegion的信息。leaks可追踪堆中的对象,从而查看内存泄漏、堆栈信息等。heap会打印出堆中所有信息,方便追踪内存占用较大的对象。malloc_history可以查看heap指令得到的对象的堆栈信息,从而方便地发现问题。 总结:malloc_history ===> Creation;leaks ===> Reference;heap & vmmap ===> Size。 MetricKit:iOS 13新推出的监控框架,用于收集和处理电池和性能指标。当用户使用APP的时候,iOS会记录各项指标,然后发送到苹果服务端上,并自动生成相关的可视化报告。通过Window -> Organizer -> Metrics可查,包括电池、启动时间、卡顿情况、内存情况、磁盘读写五部分。也可以MetricKit集成到工程里,将数据上传到自己的服务进行分析。 MLeaksFinder:通过判断UIViewController被销毁后其子view是否也都被销毁,可以在不入侵代码的情况下检测内存泄漏。 Graphics API Debugger:Google开源的一系列的Graphics调试工具,可以检查、微调、重播应用对图形驱动的API调用。 Arm Mobile Studio: 专业级GPU分析工具。
文章
缓存  ·  监控  ·  JavaScript  ·  数据挖掘  ·  定位技术  ·  API  ·  图形学  ·  Android开发  ·  iOS开发  ·  异构计算
2021-01-22
Flink Exactly-Once 投递实现浅析
5万人关注的大数据成神之路,不来了解一下吗? 5万人关注的大数据成神之路,真的不来了解一下吗? 5万人关注的大数据成神之路,确定真的不来了解一下吗? 随着近来越来越多的业务迁移到 Flink 上,对 Flink 作业的准确性要求也随之进一步提高,其中最为关键的是如何在不同业务场景下保证 exactly-once 的投递语义。虽然不少实时系统(e.g. 实时计算/消息队列)都宣称支持 exactly-once,exactly-once 投递似乎是一个已被解决的问题,但是其实它们更多是针对内部模块之间的信息投递,比如 Kafka 生产(producer 到 Kafka broker)和消费(broker 到 consumer)的 exactly-once。而 Flink 作为实时计算引擎,在实际场景业务会涉及到很多不同组件,由于组件特性和定位的不同,Flink 并不是对所有组件都支持 exactly-once(见[1]),而且不同组件实现 exactly-once 的方法也有所差异,有些实现或许会带来副作用或者用法上的局限性,因此深入了解 Flink exactly-once 的实现机制对于设计稳定可靠的架构有十分重要的意义。 下文将基于 Flink 详细分析 exactly-once 的难点所在以及实现方案,而这些结论也可以推广到其他实时系统,特别是流式计算系统。 Exactly-Once 难点分析 由于在分布式系统的进程间协调需要通过网络,而网络情况在很多情况下是不可预知的,通常发送消息要考虑三种情况:正常返回、错误返回和超时,其中错误返回又可以分为可重试错误返回(e.g. 数据库维护暂时不可用)和不可重试错误返回(e.g. 认证错误),而可重试错误返回和超时都会导致重发消息,导致下游可能接收到重复的消息,也就是 at-least-once 的投递语义。而 exactly-once 是在 at-least-once 的基础之上加上了可以识别出重发数据或者将消息包装为为幂等操作的机制。 其实消息的 exactly-once 投递并不是一个分布式系统产生的新课题(虽然它一般特指分布式领域的 exactly-once),早在计算网络发展初期的 TCP 协议已经实现了网络的可靠传输。TCP 协议的 exactly-once 实现方式是将消息传递变为有状态的:首先同步建立连接,然后发送的每个数据包加上递增的序列号(sequence number),发送完毕后再同步释放连接。由于发送端和接受端都保存了状态信息(已发送数据包的序列号/已接收数据包的序列号),它们可以知道哪些数据包是缺失或重复的。 而在分布式环境下 exactly-once 则更为复杂,最大的不同点在于分布式系统需要容忍进程崩溃和节点丢失,这会带来许多问题,比如下面常见的几个: 进程状态需要持续化到可靠的分布式存储,以防止节点丢失带来状态的丢失。 由于发送消息是一个两阶段的操作(即发送消息和收到对方的确认),重启之后的进程没有办法判断崩溃前是否已经使用当前序列号发送过消息,因此可能会导致重复使用序列号的问题。 被认为崩溃的进程有可能并没有退出,随后再次连上来变为 zombie 进程继续发送数据。 第2点和第3点其实是同一个问题,即需要区分出原本进程和重启后的进程。对此业界已经有比较成熟的解决方案: 引入 epoch 表示进程的不同世代并用分布式协调系统来负责管理。虽然还有一些衍生的细节问题,但总体来说问题都不大。但是第1点问题造成了一个比较深远的影响,即为了减低 IO 成本,状态的保存必然是微批量(micro-batching)的而不是流式的,这会导致状态的保存总是落后于流计算进度,因而为了保证 exactly-once 流计算引擎需要实现事务回滚。 状态 Exactly-Once 和端到端 Exactly-Once Flink 提供 exactly-once 的状态(state)投递语义,这为有状态的(stateful)计算提供了准确性保证。其中比较容易令人混淆的一点是状态投递语义和更加常见的端到端(end to end)投递语义,而实现前者是实现后者的前置条件。 Flink 从 0.9 版本开始提供 State API,标志着 Flink 进入了 Stateful Streaming 的时代。State API 简单来说是“不受进程重启影响的“数据结构,其命名规范也与常见的数据结构一致,比如 MapState、ListState。Flink 官方提供的算子(比如 KafkaSource)和用户开发的算子都可以使用 State API 来保存状态信息。和大多数分布式系统一样 Flink 采用快照的方式来将整个作业的状态定期同步到外部存储,也就是将 State API 保存的信息以序列化的形式存储,作业恢复的时候只要读取外部存储即可将作业恢复到先前某个时间点的状态。由于从快照恢复同时会回滚数据流的处理进度,所以 State 是天然的 exactly-once 投递。 而端到端的一致性则需要上下游的外部系统配合,因为 Flink 无法将它们的状态也保存到快照并独立地回滚它们,否则就不叫作外部系统了。通常来说 Flink 的上游是可以重复读取或者消费的 pull-based 持续化存储,所以要实现 source 端的 exactly-once 只需要回滚 source 的读取进度即可(e.g. Kafka 的 offset)。而 sink 端的 exactly-once 则比较复杂,因为 sink 是 push-based 的。所谓覆水难收,要撤回发出去的消息是并不是容易的事情,因为这要求下游根据消息作出的一系列反应都是可撤回的。这就需要用 State API 来保存已发出消息的元数据,记录哪些数据是重启后需要回滚的。 下面将分析 Flink 是如何实现 exactly-once Sink 的。 Exactly-Once Sink 原理 Flink 的 exactly-once sink 均基于快照机制,按照实现原理可以分为幂等(Idempotent) sink 和事务性(Transactional) sink 两种。 幂等 Sink 幂等性是分布式领域里十分有用的特性,它意味着相同的操作执行一次和执行多次可以获得相同的结果,因此 at-least-once 自然等同于 exactly-once。如此一来,在从快照恢复的时候幂等 sink 便不需要对外部系统撤回已发消息,相当于回避了外部系统的状态回滚问题。比如写入 KV 数据库的 sink,由于插入一行的操作是幂等的,因此 sink 可以无状态的,在错误恢复时也不需要关心外部系统的状态。从某种意义来讲,上文提到的 TCP 协议也是利用了发送数据包幂等性来保证 exactly-once。 然而幂等 sink 的适用场景依赖于业务逻辑,如果下游业务本来就无法保证幂等性,这时就需要应用事务性 sink。 事务性 Sink 事务性 sink 顾名思义类似于传统 DBMS 的事务,将一系列(一般是一个 checkpoint 内)的所有输出包装为一个逻辑单元,理想的情况下提供 ACID 的事务保证。之所以说是“理想的情况下”,主要是因为 sink 依赖于目标输出系统的事务保证,而分布式系统对于事务的支持并不一定很完整,比如 HBase 就不支持跨行事务,再比如 HDFS 等文件系统是不提供事务的,这种情况下 sink 只可以在客户端的基础上再包装一层来尽最大努力地提供事务保证。 然而仅有下游系统本身提供的事务保证对于 exactly-once sink 来说是不够的,因为同一个 sink 的子任务(subtask)会有多个,对于下游系统来说它们是处在不同会话和事务中的,并不能保证操作的原子性,因此 exactly-once sink 还需要实现分布式事务来达到所有 subtask 的一致 commit 或 rollback。由于 sink 事务生命周期是与 checkpoint 一一对应的,或者说 checkpoint 本来就是实现作业状态持久化的分布式事务,sink 的分布式事务也理所当然可以通过 checkpoint 机制提供的 hook 来实现。 Checkpoint 提供给算子的 hook 有 CheckpointedFunction 和 CheckpointListener 两个,前者在算子进行 checkpoint 快照时被调用,后者在 checkpoint 成功后调用。为了简单起见 Flink 结合上述两个接口抽象出 exactly-once sink 的通用逻辑抽象 TwoPhaseCommitSinkFunction 接口,从命名即可看出这是对两阶段提交协议的一个实现,其主要方法如下: beginTransaction: 初始化一个事务。在有新数据到达并且当前事务为空时调用。 preCommit: 预提交数据,即不再写入当前事务并准好提交当前事务。在 sink 算子进行快照的时候调用。 commit: 正式提交数据,将准备好的事务提交。在作业的 checkpoint 完成时调用。 abort: 放弃事务。在作业 checkpoint 失败的时候调用。 下面以 Bucketing File Sink 作为例子来说明如何基于异步 checkpoint 来实现事务性 sink。 Bucketing File Sink 是 Flink 提供的一个 FileSystem Connector,用于将数据流写到固定大小的文件里。Bucketing File Sink 将文件分为三种状态,in-progress/pending/committed,分别表示正在写的文件、写完准备提交的文件和已经提交的文件。 运行时,Bucketing File Sink 首先会打开一个临时文件并不断地将收到的数据写入(相当于事务的 beginTransaction 步骤),这时文件处于 in-progress。直到这个文件因为大小超过阈值或者一段时间内没有新数据写入,这时文件关闭并变为 pending 状态(相当于事务的 pre-commit 步骤)。由于 Flink checkpoint 是异步的,可能有多个并发的 checkpoint,Bucketing File Sink 会记录 pending 文件对应的 checkpoint epoch,当某个 epoch 的 checkpoint 完成后,Bucketing File Sink 会收到 callback 并将对应的文件改为 committed 状态。这是通过原子操作重命名来完成的,因此可以保证 pre-commit 的事务要么 commit 成功要么 commit 失败,不会出现其他中间状态。 Commit 出现错误会导致作业自动重启,重启后 Bucketing File Sink 本身已被恢复为上次 checkpoint 时的状态,不过仍需要将文件系统的状态也恢复以保证一致性。从 checkpoint 恢复后对应的事务会再次重试 commit,它会将记录的 pending 文件改为 committed 状态,记录的 in-progress 文件 truncate 到 checkpoint 记录下来的 offset,而其余未被记录的 pending 文件和 in-progress 文件都将被删除。 上面主要围绕事务保证的 AC 两点(Atomicity 和 Consistency),而在 I(Isolation)上 Flink exactly-once sink 也有不同的实现方式。实际上由于 Flink 的流计算特性,当前事务的未 commit 数据是一直在积累的,根据缓存未 commit 数据的地方的不同,可以将事务性 sink 分为两种实现方式。 在 sink 端缓存未 commit 数据,等 checkpoint 完成以后将缓存的数据 flush 到下游。这种方式可以提供 read-committed 的事务隔离级别,但同时由于未 commit 的数据不会发往下游(与 checkpoint 同步),sink 端缓存会带来一定的延迟,相当于退化为与 checkpoint 同步的 micro-batching 模式。 在下游系统缓存未 commit 数据,等 checkpoint 完成后通知下游 commit。这样的好处是数据是流式发往下游的,不会在每次 checkpoint 完成后出现网络 IO 的高峰,并且事务隔离级别可以由下游设置,下游可以选择低延迟弱一致性的 read-uncommitted 或高延迟强一致性的 read-committed。 在 Bucketing File Sink 的例子中,处于 in-progress 和 pending 状态的文件默认情况下都是隐藏文件(在实践中是使用下划线作为文件名前缀,HDFS 的 FileInputFormat 会将其过滤掉),只有 commit 成功后文件才对用户是可见的,即提供了 read-committed 的事务隔离性。理想的情况下 exactly-once sink 都应该使用在下游系统缓存未 commit 数据的方式,因为这最为符合流式计算的理念。最为典型的是下游系统本来就支持事务,那么未 commit 的数据很自然地就是缓存在下游系统的,否则 sink 可以选择像上例的 Bucketing File Sink 一样在下游系统的用户层面实现自己的事务,或者 fallback 到等待数据变为 committed 再发出的 micro-batching 模式。 总结 Exactly-once 是实时系统最为关键的准确性要求,也是当前限制大部分分布式实时系统应用到准确性要求更高的业务场景(比如在线事务处理 OLTP)的问题之一。目前来说流式计算的 exactly-once 在理论上已经有了很大的突破,而 Flink 社区也在积极汲取最先进的思想和实践经验。随着 Flink 在 exactly-once 上的技术愈发成熟,结合 Flink 本身的流处理特性,相信在不远的将来,除了构造数据分析、数据管道应用, Flink 也可以在微服务领域占有一席之地。 参考文献 1.Fault Tolerance Guarantees of Data Sources and Sinks2.An Overview of End-to-End Exactly-Once Processing in Apache Flink3.State Management in Apache Flink4.An Overview of End-to-End Exactly-Once Processing in Apache Flink (with Apache Kafka, too!) 欢迎您关注《大数据成神之路》
文章
存储  ·  消息中间件  ·  缓存  ·  API  ·  流计算  ·  Apache  ·  Kafka  ·  大数据  ·  网络协议  ·  数据库
2019-08-17
跳转至:
高德技术
201 人关注 | 0 讨论 | 123 内容
+ 订阅
  • 【定位不准的烦心事系列】第2篇:卫星信号弱到底是咋回事
  • 【定位不准的烦心事系列】第1篇:谈谈卫星定位的位置干扰
  • 中国计算机学会 × 高德地图 发布“POI名称生成”赛题,诚邀全球英才组队参加
查看更多 >
开发与运维
5618 人关注 | 131425 讨论 | 301661 内容
+ 订阅
  • Redis详解之 redis的简介与安装
  • JAVA环境变量配置步骤及测试(JDK的下载 & 安装 & 环境配置教程)(上)
  • 《面向对象系统分析与设计》三级项目
查看更多 >
安全
1191 人关注 | 23954 讨论 | 81223 内容
+ 订阅
  • Redis详解之 redis的简介与安装
  • 基于Spring Boot框架的在线导游预约系统(下)
  • 基于Spring Boot框架的在线导游预约系统(上)
查看更多 >
云原生
233917 人关注 | 11326 讨论 | 45241 内容
+ 订阅
  • serveless 思想 Midway.js 框架使用教程
  • (linux-x86-arm-mips)安装Tomcat 9.0
  • Prometheus 进阶|学习笔记(四)
查看更多 >
大数据
188286 人关注 | 29193 讨论 | 80401 内容
+ 订阅
  • 一图全解Kafka在zookeeper中的数据结构
  • 数据结构七大排序算法图解——插入排序动图展示
  • 华数杯2023A题思路+雅鲁藏布江数据【全网独家数据集】
查看更多 >