首页 文章 精选 留言 我的

精选列表

搜索[快速入门],共10000篇文章
优秀的个人博客,低调大师

Flink从入门到放弃(入门篇3)-DataSetAPI

首先我们来看一下编程结构: 编程结构 public class SocketTextStreamWordCount { public static void main(String[] args) throws Exception { if (args.length != 2){ System.err.println("USAGE:\nSocketTextStreamWordCount <hostname> <port>"); return; } String hostName = args[0]; Integer port = Integer.parseInt(args[1]); final StreamExecutionEnvironment env = StreamExecutionEnvironment .getExecutionEnvironment(); DataStream<String> text = env.socketTextStream(hostName, port); DataStream<Tuple2<String, Integer>> counts text.flatMap(new LineSplitter()) .keyBy(0) .sum(1); counts.print(); env.execute("Java WordCount from SocketTextStream Example"); } 上面的SocketTextStreamWordCount是一个典型的Flink程序,他由一下及格部分构成: 获得一个execution environment, 加载/创建初始数据, 指定此数据的转换, 指定放置计算结果的位置, 触发程序执行 DataSet API 分类: Source: 数据源创建初始数据集,例如来自文件或Java集合 Transformation: 数据转换将一个或多个DataSet转换为新的DataSet Sink: 将计算结果存储或返回 DataSet Sources 基于文件的 readTextFile(path)/ TextInputFormat- 按行读取文件并将其作为字符串返回。 readTextFileWithValue(path)/ TextValueInputFormat- 按行读取文件并将它们作为StringValues返回。StringValues是可变字符串。 readCsvFile(path)/ CsvInputFormat- 解析逗号(或其他字符)分隔字段的文件。返回元组或POJO的DataSet。支持基本java类型及其Value对应作为字段类型。 readFileOfPrimitives(path, Class)/ PrimitiveInputFormat- 解析新行(或其他字符序列)分隔的原始数据类型(如String或)的文件Integer。 readFileOfPrimitives(path, delimiter, Class)/ PrimitiveInputFormat- 解析新行(或其他字符序列)分隔的原始数据类型的文件,例如String或Integer使用给定的分隔符。 readSequenceFile(Key, Value, path)/ SequenceFileInputFormat- 创建一个JobConf并从类型为SequenceFileInputFormat,Key class和Value类的指定路径中读取文件,并将它们作为Tuple2 返回。 基于集合 fromCollection(Collection) - 从Java Java.util.Collection创建数据集。集合中的所有数据元必须属于同一类型。 fromCollection(Iterator, Class) - 从迭代器创建数据集。该类指定迭代器返回的数据元的数据类型。 fromElements(T ...) - 根据给定的对象序列创建数据集。所有对象必须属于同一类型。 fromParallelCollection(SplittableIterator, Class) - 并行地从迭代器创建数据集。该类指定迭代器返回的数据元的数据类型。 generateSequence(from, to) - 并行生成给定间隔中的数字序列。 通用方法 readFile(inputFormat, path)/ FileInputFormat- 接受文件输入格式。 createInput(inputFormat)/ InputFormat- 接受通用输入格式。 代码示例 ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); // 从本地文件系统读 DataSet<String> localLines = env.readTextFile("file:///path/to/my/textfile"); // 读取HDFS文件 DataSet<String> hdfsLines = env.readTextFile("hdfs://nnHost:nnPort/path/to/my/textfile"); // 读取CSV文件 DataSet<Tuple3<Integer, String, Double>> csvInput = env.readCsvFile("hdfs:///the/CSV/file").types(Integer.class, String.class, Double.class); // 读取CSV文件中的部分 DataSet<Tuple2<String, Double>> csvInput = env.readCsvFile("hdfs:///the/CSV/file").includeFields("10010").types(String.class, Double.class); // 读取CSV映射为一个java类 DataSet<Person>> csvInput = env.readCsvFile("hdfs:///the/CSV/file").pojoType(Person.class, "name", "age", "zipcode"); // 读取一个指定位置序列化好的文件 DataSet<Tuple2<IntWritable, Text>> tuples = env.readSequenceFile(IntWritable.class, Text.class, "hdfs://nnHost:nnPort/path/to/file"); // 从输入字符创建 DataSet<String> value = env.fromElements("Foo", "bar", "foobar", "fubar"); // 创建一个数字序列 DataSet<Long> numbers = env.generateSequence(1, 10000000); // 从关系型数据库读取 DataSet<Tuple2<String, Integer> dbData = env.createInput(JDBCInputFormat.buildJDBCInputFormat() .setDrivername("org.apache.derby.jdbc.EmbeddedDriver") .setDBUrl("jdbc:derby:memory:persons") .setQuery("select name, age from persons") .setRowTypeInfo(new RowTypeInfo(BasicTypeInfo.STRING_TYPE_INFO, BasicTypeInfo.INT_TYPE_INFO)) .finish()); DataSet Transformation 详细可以参考官网:https://flink.sojb.cn/dev/batch/dataset_transformations.html#filter Map 采用一个数据元并生成一个数据元。 data.map(new MapFunction<String, Integer>() { public Integer map(String value) { return Integer.parseInt(value); } }); FlatMap 采用一个数据元并生成零个,一个或多个数据元。 data.flatMap(new FlatMapFunction<String, String>() { public void flatMap(String value, Collector<String> out) { for (String s : value.split(" ")) { out.collect(s); } } }); MapPartition 在单个函数调用中转换并行分区。该函数将分区作为Iterable流来获取,并且可以生成任意数量的结果值。每个分区中的数据元数量取决于并行度和先前的 算子操作。 data.mapPartition(new MapPartitionFunction<String, Long>() { public void mapPartition(Iterable<String> values, Collector<Long> out) { long c = 0; for (String s : values) { c++; } out.collect(c); } }); Filter 计算每个数据元的布尔函数,并保存函数返回true的数据元。重要信息:系统假定该函数不会修改应用谓词的数据元。违反此假设可能会导致错误的结果。 data.filter(new FilterFunction<Integer>() { public boolean filter(Integer value) { return value > 1000; } }); Reduce 通过将两个数据元重复组合成一个数据元,将一组数据元组合成一个数据元。Reduce可以应用于完整数据集或分组数据集。 data.reduce(new ReduceFunction<Integer> { public Integer reduce(Integer a, Integer b) { return a + b; } }); 如果将reduce应用于分组数据集,则可以通过提供CombineHintto 来指定运行时执行reduce的组合阶段的方式 setCombineHint。在大多数情况下,基于散列的策略应该更快,特别是如果不同键的数量与输入数据元的数量相比较小(例如1/10)。 ReduceGroup 将一组数据元组合成一个或多个数据元。ReduceGroup可以应用于完整数据集或分组数据集。 data.reduceGroup(new GroupReduceFunction<Integer, Integer> { public void reduce(Iterable<Integer> values, Collector<Integer> out) { int prefixSum = 0; for (Integer i : values) { prefixSum += i; out.collect(prefixSum); } } }); Aggregate 将一组值聚合为单个值。聚合函数可以被认为是内置的reduce函数。聚合可以应用于完整数据集或分组数据集。 Dataset<Tuple3<Integer, String, Double>> input = // [...] DataSet<Tuple3<Integer, String, Double>> output = input.aggregate(SUM, 0).and(MIN, 2); 您还可以使用简写语法进行最小,最大和总和聚合。 Dataset<Tuple3<Integer, String, Double>> input = // [...] DataSet<Tuple3<Integer, String, Double>> output = input.sum(0).andMin(2); Distinct 返回数据集的不同数据元。它相对于数据元的所有字段或字段子集从输入DataSet中删除重复条目。 data.distinct(); 使用reduce函数实现Distinct。您可以通过提供CombineHintto 来指定运行时执行reduce的组合阶段的方式 setCombineHint。在大多数情况下,基于散列的策略应该更快,特别是如果不同键的数量与输入数据元的数量相比较小(例如1/10)。 Join 通过创建在其键上相等的所有数据元对来连接两个数据集。可选地使用JoinFunction将数据元对转换为单个数据元,或使用FlatJoinFunction将数据元对转换为任意多个(包括无)数据元。请参阅键部分以了解如何定义连接键。 result = input1.join(input2) .where(0) // key of the first input (tuple field 0) .equalTo(1); // key of the second input (tuple field 1) 您可以通过Join Hints指定运行时执行连接的方式。提示描述了通过分区或广播进行连接,以及它是使用基于排序还是基于散列的算法。如果未指定提示,系统将尝试估算输入大小,并根据这些估计选择最佳策略。 // This executes a join by broadcasting the first data set // using a hash table for the broadcast data result = input1.join(input2, JoinHint.BROADCAST_HASH_FIRST) .where(0).equalTo(1); 请注意,连接转换仅适用于等连接。其他连接类型需要使用OuterJoin或CoGroup表示。 OuterJoin 在两个数据集上执行左,右或全外连接。外连接类似于常规(内部)连接,并创建在其键上相等的所有数据元对。此外,如果在另一侧没有找到匹配的Keys,则保存“外部”侧(左侧,右侧或两者都满)的记录。匹配数据元对(或一个数据元和null另一个输入的值)被赋予JoinFunction以将数据元对转换为单个数据元,或者转换为FlatJoinFunction以将数据元对转换为任意多个(包括无)数据元。请参阅键部分以了解如何定义连接键。 input1.leftOuterJoin(input2) // rightOuterJoin or fullOuterJoin for right or full outer joins .where(0) // key of the first input (tuple field 0) .equalTo(1) // key of the second input (tuple field 1) .with(new JoinFunction<String, String, String>() { public String join(String v1, String v2) { // NOTE: // - v2 might be null for leftOuterJoin // - v1 might be null for rightOuterJoin // - v1 OR v2 might be null for fullOuterJoin } }); CoGroup reduce 算子操作的二维变体。将一个或多个字段上的每个输入分组,然后关联组。每对组调用转换函数。 data1.coGroup(data2) .where(0) .equalTo(1) .with(new CoGroupFunction<String, String, String>() { public void coGroup(Iterable<String> in1, Iterable<String> in2, Collector<String> out) { out.collect(...); } }); Cross 构建两个输入的笛卡尔积(交叉乘积),创建所有数据元对。可选择使用CrossFunction将数据元对转换为单个数据元 DataSet<Integer> data1 = // [...] DataSet<String> data2 = // [...] DataSet<Tuple2<Integer, String>> result = data1.cross(data2); 注:交叉是一个潜在的非常计算密集型 算子操作它甚至可以挑战大的计算集群!建议使用crossWithTiny()和crossWithHuge()来提示系统的DataSet大小。 Union 生成两个数据集的并集。 DataSet<String> data1 = // [...] DataSet<String> data2 = // [...] DataSet<String> result = data1.union(data2); Rebalance 均匀地Rebalance 数据集的并行分区以消除数据偏差。只有类似Map的转换可能会遵循Rebalance 转换。 DataSet<String> in = // [...] DataSet<String> result = in.rebalance() .map(new Mapper()); Hash-Partition 散列分区给定键上的数据集。键可以指定为位置键,表达键和键选择器函数。 DataSet<Tuple2<String,Integer>> in = // [...] DataSet<Integer> result = in.partitionByHash(0) .mapPartition(new PartitionMapper()); Range-Partition Range-Partition给定键上的数据集。键可以指定为位置键,表达键和键选择器函数。 DataSet<Tuple2<String,Integer>> in = // [...] DataSet<Integer> result = in.partitionByRange(0) .mapPartition(new PartitionMapper()); Custom Partitioning 手动指定数据分区。 注意:此方法仅适用于单个字段键。 DataSet<Tuple2<String,Integer>> in = // [...] DataSet<Integer> result = in.partitionCustom(Partitioner<K> partitioner, key) Sort Partition 本地按指定顺序对指定字段上的数据集的所有分区进行排序。可以将字段指定为元组位置或字段表达式。通过链接sortPartition()调用来完成对多个字段的排序。 DataSet<Tuple2<String,Integer>> in = // [...] DataSet<Integer> result = in.sortPartition(1, Order.ASCENDING) .mapPartition(new PartitionMapper()); First-n 返回数据集的前n个(任意)数据元。First-n可以应用于常规数据集,分组数据集或分组排序数据集。分组键可以指定为键选择器函数或字段位置键。 DataSet<Tuple2<String,Integer>> in = // [...] // regular data set DataSet<Tuple2<String,Integer>> result1 = in.first(3); // grouped data set DataSet<Tuple2<String,Integer>> result2 = in.groupBy(0) .first(3); // grouped-sorted data set DataSet<Tuple2<String,Integer>> result3 = in.groupBy(0) .sortGroup(1, Order.ASCENDING) .first(3); DataSet Sink 数据接收器使用DataSet用于存储或返回。使用OutputFormat描述数据接收器算子操作 。Flink带有各种内置输出格式,这些格式封装在DataSet上的算子操作中: writeAsText()/ TextOutputFormat- 按字符串顺序写入数据元。通过调用每个数据元的toString()方法获得字符串。 writeAsFormattedText()/ TextOutputFormat- 按字符串顺序写数据元。通过为每个数据元调用用户定义的format()方法来获取字符串。 writeAsCsv(...)/ CsvOutputFormat- 将元组写为逗号分隔值文件。行和字段分隔符是可配置的。每个字段的值来自对象的toString()方法。 print()/ printToErr()/ print(String msg)/ printToErr(String msg)- 在标准输出/标准错误流上打印每个数据元的toString()值。可选地,可以提供前缀(msg),其前缀为输出。这有助于区分不同的打印调用。如果并行度大于1,则输出也将与生成输出的任务的标识符一起添加。 write()/ FileOutputFormat- 自定义文件输出的方法和基类。支持自定义对象到字节的转换。 output()/ OutputFormat- 大多数通用输出方法,用于非基于文件的数据接收器(例如将结果存储在数据库中)。 可以将DataSet输入到多个 算子操作。程序可以编写或打印数据集,同时对它们执行其他转换。 示例: // text data DataSet<String> textData = // [...] // write DataSet to a file on the local file system textData.writeAsText("file:///my/result/on/localFS"); // write DataSet to a file on a HDFS with a namenode running at nnHost:nnPort textData.writeAsText("hdfs://nnHost:nnPort/my/result/on/localFS"); // write DataSet to a file and overwrite the file if it exists textData.writeAsText("file:///my/result/on/localFS", WriteMode.OVERWRITE); // tuples as lines with pipe as the separator "a|b|c" DataSet<Tuple3<String, Integer, Double>> values = // [...] values.writeAsCsv("file:///path/to/the/result/file", "\n", "|"); // this writes tuples in the text formatting "(a, b, c)", rather than as CSV lines values.writeAsText("file:///path/to/the/result/file"); // this writes values as strings using a user-defined TextFormatter object values.writeAsFormattedText("file:///path/to/the/result/file", new TextFormatter<Tuple2<Integer, Integer>>() { public String format (Tuple2<Integer, Integer> value) { return value.f1 + " - " + value.f0; } }); 使用自定义输出格式: DataSet<Tuple3<String, Integer, Double>> myResult = [...] // write Tuple DataSet to a relational database myResult.output( // build and configure OutputFormat JDBCOutputFormat.buildJDBCOutputFormat() .setDrivername("org.apache.derby.jdbc.EmbeddedDriver") .setDBUrl("jdbc:derby:memory:persons") .setQuery("insert into persons (name, age, height) values (?,?,?)") .finish() ); 序列化器 Flink自带了针对诸如int,long,String等标准类型的序列化器 针对Flink无法实现序列化的数据类型,我们可以交给Avro和Kryo 使用方法:ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); 使用avro序列化:env.getConfig().enableForceAvro(); 使用kryo序列化:env.getConfig().enableForceKryo(); 使用自定义序列化:env.getConfig().addDefaultKryoSerializer(Class<?> type, Class<? extends Serializer<?>> serializerClass) 数据类型 Java Tuple 和 Scala case class Java POJOs:java实体类 Primitive Types 默认支持java和scala基本数据类型 General Class Types 默认支持大多数java和scala class Hadoop Writables 支持hadoop中实现了org.apache.hadoop.Writable的数据类型 Special Types 例如scala中的Either Option 和Try

优秀的个人博客,低调大师

Flink从入门到放弃(入门篇4) DataStreamAPI

DataStream算子将一个或多个DataStream转换为新DataStream。程序可以将多个转换组合成复杂的数据流拓扑。DataStreamAPI和DataSetAPI主要的区别在于Transformation部分。 DataStream Transformation map DataStream→DataStream用一个数据元生成一个数据元。一个map函数,它将输入流的值加倍: DataStream<Integer> dataStream = //... dataStream.map(new MapFunction<Integer, Integer>() { @Override public Integer map(Integer value) throws Exception { return 2 * value; } }); FlatMap DataStream→DataStream 采用一个数据元并生成零个,一个或多个数据元。将句子分割为单词的flatmap函数: dataStream.flatMap(new FlatMapFunction<String, String>() { @Override public void flatMap(String value, Collector<String> out) throws Exception { for(String word: value.split(" ")){ out.collect(word); } } }); Filter DataStream→DataStream 计算每个数据元的布尔函数,并保存函数返回true的数据元。过滤掉零值的过滤器: dataStream.filter(new FilterFunction<Integer>() { @Override public boolean filter(Integer value) throws Exception { return value != 0; } }); KeyBy DataStream→KeyedStream 逻辑上将流分区为不相交的分区。具有相同Keys的所有记录都分配给同一分区。在内部,keyBy()是使用散列分区实现的。指定键有不同的方法。 此转换返回KeyedStream,其中包括使用被Keys化状态所需的KeyedStream。 dataStream.keyBy("someKey") // Key by field "someKey" dataStream.keyBy(0) // Key by the first element of a Tuple 注意: 如果出现以下情况,则类型不能成为key: 它是POJO类型但不覆盖hashCode()方法并依赖于Object.hashCode()实现 任何类型的数组 Reduce KeyedStream→DataStream 将当前数据元与最后一个Reduce的值组合并发出新值。 例如:reduce函数,用于创建部分和的流: keyedStream.reduce(new ReduceFunction<Integer>() { @Override public Integer reduce(Integer value1, Integer value2) throws Exception { return value1 + value2; } }); Fold KeyedStream→DataStream 具有初始值的被Keys化数据流上的“滚动”折叠。将当前数据元与最后折叠的值组合并发出新值。 折叠函数,当应用于序列(1,2,3,4,5)时,发出序列“start-1”,“start-1-2”,“start-1-2-3”,. .. DataStream<String> result = keyedStream.fold("start", new FoldFunction<Integer, String>() { @Override public String fold(String current, Integer value) { return current + "-" + value; } }); 聚合 KeyedStream→DataStream 在被Keys化数据流上滚动聚合。min和minBy之间的差异是min返回最小值,而minBy返回该字段中具有最小值的数据元(max和maxBy相同)。 keyedStream.sum(0); keyedStream.sum("key"); keyedStream.min(0); keyedStream.min("key"); keyedStream.max(0); keyedStream.max("key"); keyedStream.minBy(0); keyedStream.minBy("key"); keyedStream.maxBy(0); keyedStream.maxBy("key"); Window函数 关于Flink的窗口概念,我们会在后面有详细介绍。 WindowKeyedStream→WindowedStream 可以在已经分区的KeyedStream上定义Windows。Windows根据某些特征(例如,在最后5秒内到达的数据)对每个Keys中的数据进行分组。 dataStream.keyBy(0) .window(TumblingEventTimeWindows .of(Time.seconds(5))); // Last 5 seconds of data Window ApplyWindowedStream→DataStream AllWindowedStream→DataStream 将一般函数应用于整个窗口。下面是一个手动求和窗口数据元的函数。 注意:如果您正在使用windowAll转换,则需要使用AllWindowFunction。 windowedStream.apply (new WindowFunction<Tuple2<String,Integer>, Integer, Tuple, Window>() { public void apply (Tuple tuple, Window window, Iterable<Tuple2<String, Integer>> values, Collector<Integer> out) throws Exception { int sum = 0; for (value t: values) { sum += t.f1; } out.collect (new Integer(sum)); } }); // applying an AllWindowFunction on non-keyed window stream allWindowedStream.apply (new AllWindowFunction<Tuple2<String,Integer>, Integer, Window>() { public void apply (Window window, Iterable<Tuple2<String, Integer>> values, Collector<Integer> out) throws Exception { int sum = 0; for (value t: values) { sum += t.f1; } out.collect (new Integer(sum)); } }); Window ReduceWindowedStream→DataStream 将reduce函数应用于窗口并返回reduce后的值。 windowedStream.reduce (new ReduceFunction<Tuple2<String,Integer>>() { public Tuple2<String, Integer> reduce(Tuple2<String, Integer> value1, Tuple2<String, Integer> value2) throws Exception { return new Tuple2<String,Integer>(value1.f0, value1.f1 + value2.f1); } }); 提取时间戳 关于Time我们在后面有专门的章节进行介绍 DataStream→DataStream 从记录中提取时间戳,以便使用使用事件时间语义的窗口。 stream.assignTimestamps (new TimeStampExtractor() {...}); Partition 分区 自定义分区DataStream→DataStream 使用用户定义的分区程序为每个数据元选择目标任务。 dataStream.partitionCustom(partitioner, "someKey"); dataStream.partitionCustom(partitioner, 0); 随机分区DataStream→DataStream 根据均匀分布随机分配数据元。 dataStream.shuffle(); Rebalance (循环分区)DataStream→DataStream 分区数据元循环,每个分区创建相等的负载。在存在数据倾斜时用于性能优化。 dataStream.rebalance(); rescaleDataStream→DataStream 如果上游 算子操作具有并行性2并且下游算子操作具有并行性6,则一个上游 算子操作将分配元件到三个下游算子操作,而另一个上游算子操作将分配到其他三个下游 算子操作。另一方面,如果下游算子操作具有并行性2而上游 算子操作具有并行性6,则三个上游 算子操作将分配到一个下游算子操作,而其他三个上游算子操作将分配到另一个下游算子操作。 在不同并行度不是彼此的倍数的情况下,一个或多个下游 算子操作将具有来自上游 算子操作的不同数量的输入。 请参阅此图以获取上例中连接模式的可视化: dataStream.rescale(); 广播DataStream→DataStream 向每个分区广播数据元。 dataStream.broadcast();

优秀的个人博客,低调大师

Themosis —— WordPress 快速开发框架

Themosis 是一种框架,可帮助开发者使用WordPress更快地开发网站和 Web 应用程序。Themosis 框架使用优雅而简单的代码语法,可帮助开发者构建和组织代码,并允许开发者更好地管理和扩展 WordPress 网站和应用程序。 Themosis 框架是一款面向任何级别的 WordPress 开发人员的工具。但是,您对 WordPress 和 PHP 的了解越多,使用起来就越容易。 要求: 服务器要求与WordPress基本相同,但额外增加了一些: PHP >= 7.1 IntlPHP 扩展 我们建议至少使用 PHP 7.2+ 安装 Composer Themosis 框架使用Composer管理其依赖项并轻松加载其文件。请按照此处的说明在计算机上本地或全局安装 Composer。 在 Windows 上,您可以使用 ComposerWindows 安装程序。 我们建议在系统上全局安装 Composer。 安装 Themosis 框架 打开Terminal或Console执行以下命令: composer create-project themosis/themosis my-project-name 这将在计算机上调用的目录创建一个my-project-name,并自动下载最新的 WordPress 版本以及最新的 Themosis 框架版本及其依赖项。 安装Themosis 默认情况下,不再使用 Composer 安装默认主题。 相反,CLI 工具现在与框架捆绑在一起并安装在项目根目录下。要安装新主题,请从终端运行以下命令: php console theme:install 该命令将要求用户输入主题名称,然后将主题下载并解压缩到htdocs/content/themes目录中。

优秀的个人博客,低调大师

如何快速定位程序Core?

导读:程序core是指应用程序无法保持正常running状态而发生的崩溃行为。程序core时会生成相关的core-dump文件,是程序崩溃时程序状态的数据备份。core-dump文件中包含内存、处理器、寄存器、程序计数器、栈指针等状态信息。本文将介绍一些利用core-dump文件定位程序core原因的方法和技巧。 全文7023字,预计阅读时间 13分钟。 一、程序Core定义及分类 程序core是指应用程序无法保持正常running状态而发生的崩溃行为。程序core时会生成相关的core-dump文件,core-dump文件是程序崩溃时程序状态的状态数据备份。core-dump文件包含内存、处理器、寄存器、程序计数器、栈指针等状态信息。我们可以借助core-dump文件来分析定位程序Core的原因。 这里我们从三个方面对程序Core进行分类:机器、资源、程序Bug。下表对常见的Core原因进行了分类: 二、函数栈介绍 当我们打开core文件时,首先关注的是程序崩溃时的函数调用栈状态,为了方便理解后续定位core的一些技巧,这里先简单介绍一下函数栈。 2.1 寄存器介绍 目前生产环境都为64位机,这里只介绍64位机的寄存器,如下: 对于x86-64架构,共有16个64位寄存器,每个寄存器的用途并不单一,如%rax通常保存函数返回结果,但也被应用于imul和idiv指令。这里重点关注%rsp(栈顶指针寄存器)、%rbp(栈底指针寄存器)、%rdi、%rsi、%rdx、%rcx、%r8、%r9(分别对应第1~6函数参数)。 Callee Save说明是否需要被调用者保存寄存器的值。 2.2 函数调用 2.2.1 调用函数栈帧: 在调用一个函数时首先进行的是参数压栈,参数压栈的顺序跟参数定义的顺序相反。注意,并不是参数一定会压栈,在x86-64架构中会针对可以使用寄存器传递的变量,直接通过寄存器传值,如数字、指针、引用等。 接着是**返回地址压栈,**返回地址为被调用函数执行完后,调用函数执行的下一个指令地址。这里牢记返回地址的位置,后续章节会利用到这个返回地址的特性。 针对上面的介绍举个例子说明: 如上图,在main函数中调用了foo函数,首先对参数压栈,三个参数都可以直接用寄存器传递(分别对应%edi、%esi、%edx),然后call指令将下一个指令压栈。 2.2.2 被调用函数栈帧: 被调用函数首先会将上一个函数的栈底指针(%rbp)保存,即%rbp压栈。然后再保存需要被保存的寄存器值,即Callee Save为True的寄存器。接着为临时变量、局部变量申请栈空间。 针对被调用函数,举个例子说明: 如上图,在foo函数执行时,先对main函数的%rbp压栈,再把寄存器中的参数值存放到局部变量(a, b, c)中。 2.3 总结 通过对函数调用的简单介绍,我们可以发现函数栈是一个缜密且脆弱的结构,内存结构必须按照严格的方式被访问,如稍有不慎就可能导致程序崩溃。 三、GDB定位Core 这一节将介绍从core文件打开到定位全流程中可能会遇到的问题以及解决技巧。 3.1 Core文件 core文件在哪里? 查看“/proc/sys/kernel/core_pattern”确定core文件生成规则。 3.2 变量打印 程序debug过程中常常要查看各种变量(内存、寄存器、函数表等)的值是否正确,维持单独用一节介绍下常用的变量打印方法以及一些冷门小技巧。 3.2.1 print命令 print [Expression] print $[Previous value number] print {[Type]}[Address] print [First element]@[Element count] print /[Format] [Expression] Format格式: o - 8进制 x - 16进制 u - 无符号十进制 t - 二进制 f - 浮点数 a - 地址 c - 字符 s - 字符串 3.2.2 x命令 x /<n/f/u> <addr> n:是正整数,表示需要显示的内存单元的个数,即从当前地址向后显示n个内存单元的内容,一个内存单元的大小由第三个参数u定义。 f:表示addr指向的内存内容的输出格式, s对应输出字符串,此处需特别注意输出整型数据的格式: x 按十六进制格式显示变量. d 按十进制格式显示变量。 u 按十进制格式显示无符号整型。 o 按八进制格式显示变量。 t 按二进制格式显示变量。 a 按十六进制格式显示变量。 c 按字符格式显示变量。 f 按浮点数格式显示变量。 u:就是指以多少个字节作为一个内存单元-unit,默认为4。 u还可以用被一些字符表示: 如b=1 byte, h=2 bytes,w=4 bytes,g=8 bytes.<addr>:表示内存地址。 3.2.3 容器对象打印 利用上面的print和x命令,再结合容器的数据结构,我们就能知道容器的详细信息。这里举个完整打印二进制string的例子,string的数据结构如下: string为空时,_M_dataplus._M_p是指向nullptr的。当赋值后会在堆上申请一段内存,分为两段,前半段是meta信息(类型为std::string::_Rep),如length、capacity、refcount,后半段为数据区,_M_p指向数据区。 通常情况下非二进制的string,直接print即可显示数据内容,但当数据为二进制时,'\0'会截断打印内容。因此,打印二进制string的首要任务是确认string的size。 string的size信息保存在std::string::_Rep结构体中,根据上面的数据结构可以发现,_Rep与_M_dataplus._M_p相差一个结构体大小,因此打印_Rep结构体的命令为: #先把_M_p转成_Rep指针,再让指针向低地址偏移一个结构体大小 p *((std::string::_Rep*)(s._M_dataplus._M_p) - 1) 找到string的size(_M_length)后,再通过x命令打印相关的内存区即可,命令为: #这里的n是_Rep._M_length x /ncb s._M_dataplus._M_p 运行效果如下: 为了方便,这里推荐一个方便的脚本:stl-views.gdb(链接:https://sourceware.org/gdb/wiki/STLSupport?action=AttachFile&do=view&target=stl-views-1.0.3.gdb,直接在gdb终端source stl-views.gdb即可,支持常见的容器打印,如vector、map、list、string等。 3.2.4 静态变量打印 程序中经常会使用到静态变量,有时我们需要查看某个静态对象的值是否正确,就涉及到静态对象的打印。看如下例子: void foo() { static std::string s_foo("foo"); } 这里可以借助nm -C ./bin | grep xx找到静态变量的内存地址,再通过gdb的print打印。 3.2.5 内存dump dump [format] memory filename start_addr end_addr dump [format] value filename expr format一般使用binary,其他的可以查看gdb手册。 比如我们可以结合上面查看string内容的例子dump整个string数据到文件中。 dump binary memory file1 s._M_dataplus._M_p s._M_dataplus._M_p + length 如果想查看文件内容的话可把vim -b和xxd结合使用。 接上面string的例子,举一个dump string内存数据到文件的例子: 3.3 定位代码行 定位core的原因,首先要定位崩溃时正在执行的代码行,这一节主要介绍一些定位代码行的方法。通常情况下直接通过gdb的breaktrace即可一览整个函数栈,但有时候函数栈信息并非如此清晰明了,这时就可利用一些小技巧来查看函数栈。 3.3.1 去编译优化 有时候会发现core的函数栈跟实际的代码行不匹配,如果是在线下环境中,可以尝试把编译优化设置成**-O0**,然后再重新复现core问题。 3.3.2 程序****计数器 + addr2line 对于线上core问题,一般没法再对程序进行去编译优化操作,只能在现有的core文件基础上进行代码定位。这一节我们采用一个例子来介绍如何使用程序计数器 + addr2line来定位代码行。 从截图可以发现frame 20指示的代码行与实际的代码行是不匹配的,定位步骤如下: # 跳转到第20号栈 frame 20 # 使用display命令显示程序计数器 display /i $rip # 使用addrline工具做地址转换 shell /opt/compiler/gcc-8.2/bin/addr2line -e bin address 3.3.3 函数栈修复 有时候我们会发现函数调用栈里面会出现很多??的情况,这常发生于栈被写花,某些情况下手动进行修复。函数栈的修复利用的函数栈内存分布知识,见第一节。 ----------------------------------- Low addresses ----------------------------------- 0(%rsp) | top of the stack frame | (this is the same as -n(%rbp)) ---------|-------------------------- n(%rbp) | variable sized stack frame- 8(%rbp) | varied 0(%rbp) | previous stack frame address 8(%rbp) | return address ----------------------------------- High addresses 从上面的栈示意图可以发现,利用%rbp寄存器即可找到上一个函数的返回地址和栈底指针,再利用addr2line命令找到对应的代码行。这里举一个例子: #首先找到当前被调用栈上一个栈的栈底指针值和返回地址 x /2ag $rbp # 2个单位,a=十六进制,g=8字节单元 #使用上一条命令得到的栈底指针值依次递归 x /2ag address 3.3.4 无规律core栈 无规律core栈问题一般发生于堆内存写坏。函数调用是一个非常精密的过程,任何一个位置发生非预期的读写都会导致程序崩溃。这里可以举个小例子来说明: int main(int argc, char* argv[]) { std::string s("abcd"); *reinterpret_cast<uint64_t*>(&s) = 0x11; return 0; } 上面的例子core在string析构上,原因是因为string的_M_ptr被改写成了0x11,析构流程变成了非法内存操作。 同理,由于进程堆空间是共享的,一个线程对堆的非法操作就可能会影响另一个线程的正常操作,由于堆分配的随机性,表现出来的现象就是无规律core栈。 针对无规律core栈最好的方式还是借助AddressSanitizer。 #设置编译参数CXXFLAGS CXXFLAGS="-fPIC -fsanitize=address -fno-omit-frame-pointer" #设置链接参数 LDFLAGS="-lasan" # 设置启动环境变量 export ASAN_OPTIONS=halt_on_error=0:abort_on_error=1:disable_coredump=0 # 启动 LD_PRELOAD=/opt/compiler/gcc-8.2/lib/libasan.so ./bin/xxx 3.3.5 总结 上面提到的几种方法都是为了找到具体的问题代码行,为后续分析core的具体原因提供线索。 3.4 定位Core原因 这一节主要介绍定位Core原因的方法以及一些常见原因的介绍。 3.4.1 确认信号量 从上面的Core分类我们可以发现某些场景的core是由于机器故障导致的,如SIGBUS,因此可以先通过信号量排除掉一些core原因。 3.4.2 定位异常汇编指令 通过上面的代码行定位我们可以大致找到程序core在哪一行,比较简单的core直接print程序上下文即可找到core的原因。 但有些场景下,通过排查上下文无任何异常,这个时候就需要准确定位具体的异常汇编指令,根据指令找原因。 查看汇编指令比较简单的方法是使用layout asm命令,frame指向那个栈,就显示对应栈的汇编。这里举个core例子,如下: 程序显示core在start函数,查看相关上下文变量均无异常。使用layout asm打开正在执行的汇编指令,如下: 查看汇编定位到程序core在mov指令,mov指令上一个指令为sub,为栈申请了3M空间,怀疑是栈空间不足。采用frame 0的%rsp - frame N的%rbp排查为栈空间不足。 通过上面的例子,可以发现定位异常汇编指令位置后,我们能够把异常点进一步压缩,定位到是哪个指令、变量、地址导致的core问题。 3.4.3 排查异常变量 通过上面的操作我们可以准确定位到具体是哪一行代码的哪一条指令出现了问题,根据异常指令我们可以排查相关的变量,确定变量值是否符合预期。 这里举一个比较经典的空指针例子,如下: int main(int argc, char* argv[]) { int* a = nullptr; *a = 1; return *a; } 通过汇编指令我们可以发现是movl $0x1, (%rax)出现了问题,%rax的值来自于0x8(%rbp),x命令打印相关的地址就可以发现为空指针错误。 3.4.4 查看被优化变量 通常情况下程序都是开启了编译优化的,就会出现变量无法被print,提示变量被优化,有时可利用汇编 + 寄存器的方式查看被优化的变量。 这里举一个例子说明下: void foo(char const* str) { char buf[1024] = {'\0'}; memcpy(buf, str, sizeof(buf)); } int main(int argc, char* argv[]) { foo("abcd"); return 0; } 通常情况下在foo函数内部,str变量是会直接别优化掉的,因为可以直接利用%rdi寄存器传递参数。为了能够打印出str的值,这个时候我们可以借助汇编 + 寄存器的方式找到具体的变量值,如下: 首先找到main函数调用foo函数的参数压栈汇编:mov $0x402011, %edi,这里的0x402011即为str的内存地址,通过x命令即可显示str的值了。 比较复杂的场景可能没法直接找到被优化变量,这时可以采用汇编回溯的方式找到变量。 3.4.5 异常函数地址排查 有时的core问题是因为数据异常导致,有时也可能是优化函数地址导致,如调用虚函数地址错误、函数返回地址错误、函数指针值错误。 异常函数地址排查同理于异常变量排查,根据汇编指令确认调用是否异常即可。这里举一个虚函数地址异常的例子,如下: class A { public: virtual ~A() = default; virtual void foo() = 0; }; class B : public A { public: void foo() {} }; int main(int argc, char* argv[]) { A* a = new B; a->foo(); A* b = new B; *reinterpret_cast<void**>(b) = 0x0; b->foo(); return 0; } 从汇编指令看是core在了mov (%rax), %rax,结合指令上下文可发现是在虚函数地址寻址操作,对比两个变量的虚函数表即可发现是函数地址load错误导致的core。 3.4.6 总结 定位core的基本流程可总结为以下几步: 明确core的大致触发原因。机器问题?自身程序问题? 定位代码行。哪一行代码出现了问题。 定位执行指令。哪一行指令干了什么事。 定位异常变量。指令不会有问题,是指令操作的变量不符合预期。 善于利用汇编指令以及**打印指令(x、print、display)**可以更有效的定位Core。 参考资料: 汇编查看工具:https://godbolt.org/ https://cppinsights.io/ 标准GDB文档:https://sourceware.org/gdb/current/onlinedocs/gdb/ 招聘信息: 欢迎加入百度移动生态事业群内容中台架构团队,我们常年需求后端、C++、模型架构、大数据、性能调优的同学、社招,实习,校招都要哦 简历投递邮箱:geektalk@baidu.com (投递备注【内容架构】) 推荐阅读: |面向大规模商业系统的数据库设计和实践 |百度爱番番移动端网页秒开实践 |解密百TB数据分析如何跑进45秒 ---------- END ---------- 百度Geek说 百度官方技术公众号上线啦! 技术干货 · 行业资讯 · 线上沙龙 · 行业大会 招聘信息 · 内推信息 · 技术书籍 · 百度周边 欢迎各位同学关注

优秀的个人博客,低调大师

混合云的快速指南

混合云模型为企业提供了一种利用公共云和私有云优势的方法。 混合云带来了两全其美的优势。 在最初的几年中,云计算技术日渐盛行,得到了IT和金融专业人士的称赞。但是,那些使用极其敏感或关键任务系统的人仍然不愿意使用该技术。企业开始做的是保持平衡并采用混合模型,而不是全盘采用内部部署数据中心或公共系统。 混合云是一种利用私有云和公共云在同一组织内执行不同功能的集成云服务。所有云计算服务应在不同程度上提供一定的效率,但公共云服务可能比私有云更具成本效益和可扩展性。借助混合云,组织可以通过将公共云服务用于所有不敏感的操作,而仅在需要时依靠私有云,从而确保所有平台无缝集成,从而最大限度地提高效率。 混合云是指由本地基础设施、私有云服务和公共云(例如Amazon Web Services(AWS)或Microsoft Azure)组成的混合计算、存储和服务环境,其中各个平台之间进行了编排。在数据中心中结合使用公共云,本地计算和私有云意味着企业拥有混合云基础设施。 混合云是一种计算环境,它通过允许在数据和应用程序之间共享而将公共云和私有云结合在一起。当计算和处理需求波动时,混合云计算使企业能够将其内部部署基础设施无缝扩展到公共云,以处理任何溢出。无需让第三方数据中心访问其全部数据。组织可以在基本和非敏感计算任务上获得公共云的灵活性和计算能力,同时将关键业务应用程序和数据安全地保留在公司防火墙的内部。 混合云通常是指公共云服务和内部私有云的结合;但是,混合云也可以由不同提供商提供的两个公共云组成,甚至可以由云和传统IT组成。实际上,当前将传统IT基础设施上的现有系统与公共云服务相结合的设置是混合云最常见的用例。 混合云使企业可以使用更广泛的IT服务组合。例如,企业可能在私有云中运行关键任务工作负载,但使用公共云提供商的数据库或归档服务。 这意味着,公共云的即付即用的可扩展性非常适合繁忙或不可预测的流量,并且可以降低IT成本。当企业需要增强安全性并需要对关键业务应用程序和数据进行最终控制时,请合并私有云。利用单租户专用服务器的性能,企业可以在裸机上获得超快的性能、安全性和可靠性。混合云方法可以统一这两种方法,可以为企业提供经济高效的安全解决方案。 混合云用例是大数据处理。例如,一家公司可以使用混合云存储来保留其累积的业务、销售、测试和其他数据,然后在公共云中运行分析查询,这可以扩展Hadoop或其他分析集群以支持要求苛刻的分布式计算任务。 尽管有好处,混合云计算仍会带来技术,业务和管理方面的挑战。这需要本地IT员工和云架构师的大量专业知识。诸如数据库,帮助台系统和其他工具之类的其他软件的实施可能会使私有云进一步复杂化。此外,企业对私有云的技术支持负全部责任,并且必须适应公共云API的任何更改以及服务随时间的变化。 此外,可能存在潜在的连接问题、服务等级协议(SLA)违规以及其他可能的服务中断。为了减轻这些风险,组织可以设计与多个公共云提供商进行互操作的混合云工作负载。在某些情况下,企业需要重新设计混合云计划的工作负载,以解决特定公共云提供商的API。 迁移到混合云可以节省资金,并使企业更高效、,更敏捷。与私有云相比,公共云可能会提供更重要的规模经济,例如集中管理,因此具有更高的成本效率。因此,混合云使企业可以尽可能多地访问这些节省的业务功能,同时仍保持敏感操作的安全性。

优秀的个人博客,低调大师

借助URLOS快速安装AliSQL

环境需求 最低硬件配置:1核CPU,1G内存(1+1)提示:如果你的应用较多,而主机节点的硬件配置较低,建议在部署节点时开通虚拟虚拟内存; 生产环境建议使用2G或以上内存; 推荐安装系统:Ubuntu-16.04、Ubuntu-18.04、CentOS7.X、Debian9X的64位的纯净的操作系统; URLOS安装 curl -LO www.urlos.com/iu && sh iu AliSQL安装流程 登录URLOS系统后台,在应用市场中选择别名并搜索“AliSQL”,找到之后,选择合适的版本点击安装按钮 填写服务名称、选择运行节点、服务端口、选择智能部署 填写数据库root用户密码 然后点击“提交”按钮,等待部署完成。

优秀的个人博客,低调大师

借助URLOS快速安装Ruby

环境需求 最低硬件配置:1核CPU,1G内存(1+1)提示:如果你的应用较多,而主机节点的硬件配置较低,建议在部署节点时开通虚拟虚拟内存; 生产环境建议使用2G或以上内存; 推荐安装系统:Ubuntu-16.04、Ubuntu-18.04、CentOS7.X、Debian9X的64位的纯净的操作系统; URLOS安装 curl -LO www.urlos.com/iu && sh iu Ruby安装流程 登录URLOS系统后台,在应用市场中搜索“Ruby”,找到之后,直接点击安装按钮 填写服务名称、选择运行节点、选择智能部署 填写ssh密码(这里的密码是root密码) 然后点击“提交”按钮,等待部署完成;

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

用户登录
用户注册