如何调整 Hadoop?阅读本文,你会满意的。
「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」
引子
小明又来了~
小明转行大数据后成功找到了一份大数据的工作,这是一个小公司,公司的大数据平台使用的是比较经典的 Apache Hadoop 框架,存储层是HDFS,计算引擎是MapReduce,调度引擎是YARN,领导给小明安排的活是接管 Hadoop 集群的运维加部分开发工作,第一份任务是希望小明能对现有的Hadoop集群调下优,因为 Hadoop 集群运行时间比较长了,出于稳定性和成本的考虑不会去更换平台的架构,但是现有的集群性能方面不太够满足需要了,于是就希望小明能做点什么,毕竟是一家小公司嘛。
小明接手平台后,先在网上搜索了一下 Hadoop 怎么去调优,发现很多博客都不全面。
于是小明又想起了自己的远房表哥大明,大明听说后,给小明传了一份文档。
文档内容如下:
正文
Hadoop 集群的调优可以从四方面着手:
- Linux 调优
- HDFS 调优
- MapReduce 调优
- YRAN 调优
Linux 操作系统调优
修改内核参数
管理员通常会保持 Linux 服务器内核的默认设置,这并不是一个明智的做法,因为这有可能对集群的性能产生不利的影响!
下表给出了在 Hadoop 生产集群中推荐使用的 Linux 内核参数配置。
Linux 内核参数配置 | 参数说明 |
---|---|
fs.file-mx=6815744 | 文件描述符总数 |
fs.aio-max-nr=1048576 | 最大并发I/O请求数 |
net.core.rmem_default=262144 | 操作系统接收缓冲区的默认大小 |
net.core.wmem_default=262144 | 操作系统发送缓冲区的默认大小 |
net.core.rmem_max=16777216 | 系统接收缓冲区最大值 |
net.core.wmem_max=16777216 | 系统发送缓冲区最大值 |
net.ipv4.tcp_rmem=409626214416777216 | 接收窗口尺寸的最小、默认、最大值 |
net.ipv4.tcp_wmem=409626214416777216 | 发送窗口尺寸的最小、默认、最大值 |
增加文件限制
为了避免集群中的任何文件描述符错误,需要增加单个用户或进程一次可以打开的文件数量的限制。
默认值只有 128 。
可以使用以下命令检査当前限制(第一个为软限制第二个为硬限制)。
[root@hadoop ~]# ulimit -Sn
1024
[root@hadoop ~]# ulimit -Hn
4096
[root@hadoop ~]#
需要将 ulimit 值至少提高至4096( Hortonworks等推荐10000或者更多)。
可以通过编辑 /etc/security/limits.conf 文件来执行此操作,如下所示:
Soft nofile 4096
Hard nofile 4096
且更改了内核设置,则可以通过执行以下命令来动态加载新设置
[root@hadoop ~]# sysctl -p
可以通过发出以下命令来确认新的内核设置:
[root@hadoop ~]# sysctl -a
磁盘设置
确保在挂载所有磁盘时使用 noatime
时间以及挂载所有目录时使用 nodir
时间。
这样, 可以避免在对 Linux 文件系统中的文件或目录进行读取操作时的不必要写入操作,从而提高集群性能。
测试磁盘I/O速度
使用 hdparm -t
命令测试磁盘速度,如下所示
$ hdparm -t /dev/sdal
如果没有看到 70MB/S 以上的速度,这意味着有一个潜在的问题存在
检查服务器的 BIOS 设置
通过确保不启用磁盘驱动器的 IDE 仿真等功能来保证服务器 BIOS 为最佳性能配置。
存储和系统管理员会关注这个配置。
注意:在挂磁盘驱动器之前,将所有 Hadoop 目录下的文件权限更改为 700 这样在却驱动器时,向这些驱动器写入的任何进程都不会占满操作系统。
网卡绑定
为了提高吞吐量和弹性,最好通过执行NC绑定来组合网络接口。
启用 NTP
确保集群所有节点的时钟是同步的。
如果集群无法访问 Intenet ,则必须将集群中的一个服务器设置为 NTP 服务器。
通过编辑 /etc/sysconfig/ntpd
文件启用 NTP 守护进程来同步所有集群节点上的网络时间同步所有集群节点上的网络时间对于诸如 ZooKeeper 、 Kerberos 和 HBase 之类的应用程序至关重要。
当通过日志文件对集群进行故障排除时,在集群中使用同步时间也很重要。
注意尽管不是必须的,但最好使用单独的虚拟局域网( VLAN )为 Hadoop 提供专用交换基础设施。
在 CentOS8 中默认不再支持 ntp 软件包,时间同步将由 chrony 来实现,可以通过修改 /etc/chrony.conf 实现。
检查 DNS
使用主机名而不是 IP 地址来标识集群节点。
在理想情况下,集群中的所有节点都必须被配置 DNS 和反向 DNS 。
确保将所有主机名设置为完全限定域名( FQDN )。
以下是一个例子
# hostname -- fqdn
hadoop1.localdomain
#
如果由于某些原因无法配置 DNS ,确保编辑所有节点的 /etc/hosts 文件,将集群中的所有节点列入其中。
每个主机必须能够执行正向查找(利用主机名)和反向査找(利用 IP 地址)。
主机命令可帮助验证正向和反向查找,如下所示
# host hadoop1
hadoop1.localdomain has address 10.192.2.29
# host 10.192.2.29
10.192.2.29 in-addr.arpa domain name pointer hadoop1.localdomain
#
由于 Hadoop 大量使用基于网络的服务(如 DNS ),因此启用名称服务器缓存守护程序( nscd )以降低名称解析延迟是个好主意
禁用 swap
理想情况下,服务器都不应该 swap ,尤其是 DataNode 。
可以使用以下命令在这些服务器上完全禁用该功能。
# swapoff - a
可以使用以下命令检查服务器上的 swap 状态
# swapon - s
默认情况下,大多数 Linux 操作系统的 swappiness
被设置为 60 。
如果 swappiness 设置为零,除非内存不足, Linux 将避免使用磁盘,而设置为 100 表示操作系统立即将程序切换到磁盘。
我们知道,设置为 60 意味着从内存使用量达到操作系统分配的内存的半左右的时间开始,操作系统会相当频繁地使用磁盘上的交换文件。
例如,如果将 swappiness 调低到 10 ,则只有当 RAM 占用率达到 90 %左右时,操作系统才会使用磁盘上的交换文件。
Linux 管理员可以将以下设置添加到 /etc/sysctl.conf 文件中来更改系统的 swappiness
vm.swappiness=10
管理员必须重新启动服务器才能使新的 swappiness 设置生效。
对于将 swappiness 值设置为多低,没有特别明确的强制规定。
Cloudera 专家建议将其设置为 1
。
禁用 SELinux
虽然这并不是一个绝对的要求,但 SELinux 有时会干扰 Hadoop 的安装,所以在开始安装 Hadoop 之前最好禁用 SELinux 。
此外, SELinux 会对集群造成 7 %~ 10 %的性能损失。
可以执行以下命令获取当前的 SELinux 状态
# getenforce
如果当前模式的值为 enforcing ,则 SELinux 处于启用状态。
可以将状态更改为 permissive
来禁用它,如下所示:
# setenforce 0
关闭 IPv6
需要为某些网络相关的 Hadoop 配置参数设置值 0.0.0.0 ,将 Hadoop 绑定到服务器的 IPv6 地址。
如果没有连接到 IPv6 网络,则可以简单地禁用集群节点上的 IPv6 。
可以通过编辑 /etc/sysctl.conf
文件并在文件末尾添加以下行来禁用 IPv6 :
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.io.disable_ipv6 = 1
对 sysctl.conf 文件进行更改后,必须重新启动服务器。
重新启动后,执行以下命令来检查更改是否成功:
$ cat /proc/sys/net/ipv6/conf/all/disable_ipv6
如果禁用 IPv6 ,输出应该为 1 ,否则为 0 。
也可以通过为环境变量 HADOOP_OPTS 添加以下值来禁用 Hadoop 的 IPv6 。
将此行添加到集群的 hadoop-env.sh 文件中:
export HADOOP_OPTS=-Djava.net.preferIPv4Stack=true
禁用 IP 表
在安装 Hadoop 时,最好关掉网络防火墙(并进行检査),如下所示
# service iptables stop
# service iptables status
安装完成后,可以重新启用 IP 表。
设置 Limits
可以通过 shell 来限制用户能利用的集群资源。
为此,可以编辑 /etc/security/limits.conf
文件,该文件规定了如何限制用户使用资源。
limits.conf 文件用于配置重要的操作系统属性的“软”和“硬”限制,如文件大小、堆栈大小和进程的优先级(精度)等,如下所示。
将以下行添加到 /etc/security/limits.conf
文件中:
soft nofile 32768
hard nofile 32768
soft nproc 32768
soft nproc 32768
nofile 属性限制每个用户进程打开的文件描述符的数量, nproc指定最大进程数。
软限制设置意味着警告,硬限制设置是实际的资源限制。
关闭透明大页(THP)压缩
据 Cloudera 和 Hortonworks 的专家介绍,THP压缩会降低 Hadoop的性能。
所以,禁用碎片整理是一个很好的做法,具体方法如下所示(将此行添加到 /etc/rc.local 文件):
$ echo 'never'; defrag_file_pathname
检查连通性
检查节点之间的无密码连接,以确保对SSH进行了正确的配置。
HDFS 调优
1. hdfs-site.xml
<propertv>
<name>dfs.block.size</name>
<value>134217728</value>
</property>
解释: 该参数表示 Hadoop 的文件块大小,通常设为128MB或者256MB。
<property>
<name>dfs.namenode.handler.count</name>
<value>40</value>
</property>
解释: 该参数表示 NameNode 同时和 DataNode 通信的线程数,默认为10,将其增大为40。
<property>
<name>dfs.datanode.max.xcievers</name>
<value>65536</value>
</property>
解释: dfs.datanode.max.xcievers 对于 DataNode 来说就 如同 Linux 上的文件句柄的限制,当 DataNode上面的连接数超过配置中的设置时, DataNode就会拒绝连接,修改设置为65536。
<property>
<name>dfs.datanode.balance.bandwidthPerSe</name>
<value>20485760</value>
</property>
解释: 该参数表示执行 start-balancer.sh 的带宽,默认为1048576(1MB/s),将其増大到20MB/s
<property>
<name>dfs.replication</name>
<value>3</value>
</property>
解释: 该项参数表示控制HDFS文件的副本数,默认为3,当许多任务同时读取一个文件时,读取可能会造成瓶颈,这时增大副本数可以有效缓解这种情况,但是也会造成大量的磁盘空间占用,这时可以只修改 Hadoop 客户端 的配置,这样,从 Hadoop 客户端上传的文件的副本数将以 Hadoop 客户端的为准
<property>
<name>dfs.datanode.max.transfer.threads</name>
<value>4096</value>
</property>
解释: 该参数表示设置 DataNode 在进行文件传输时最大线程数,通常设置为8192,如果集群中有某台 DataNode 主机的这个值比其他主机的大,那么出现的问题是,这台主机上存储的数据相对别的主机比较多,导致数据分布不均匀的问题,即使 balance 仍然会不均匀。
2. core-site.xml
<property>
<name>io.file.buffer.size</name>
<value>131072</value>
</property>
解释: Hadoop的缓冲区大小用于 Hadoop 读HDFS的文件和写HDFS的文件,还有map 的中间结果输出都用到了这个缓冲区容量,默认为4KB,增加为128KB。
MapReduce 调优
使用 Hadoop 进行大数据运算,当数据量极大时,那么对 MapReduce 性能的调优重要性不言而喻,尤其是 Shuffle 过程中的参数配置对作业的总执行时间影响特别大。
下面总结一些和 MapReduce 相关的性能调优方法,主要从 5 个方面考虑:
数据输入、 Map 阶段、 Reduce 阶段、 Shuffle 阶段和其他调优属性。
1. 数据输入
在执行 MapReduce 任务前,将小文件进行合并,大量的小文件会产生大量的 Map 任务,増大 Map 任务装载的次数,而任务的装载比较耗时,从而导致 MapReduce 运行速度较慢。
因此采用 ==CombineTextInputFormat== 来作为输入,解决输入端大量的小文件场景
2. Map 阶段
- 减少溢写( spill )次数: 通过调整 io.sort.mb 及 sort.spill.percent 参数值,增大触发 spill 的内存上限,减少 spill 次数,从而减少磁盘 I / O 。
- 减少合并( merge )次数: 通过调整 io.sort. factor 参数,增大 merge 的文件数目,减少 merge 的次数,从而缩短 MR 处理时间。
- 在 Map 之后,不影响业务逻辑前提下,先进行 combine 处理,减少 I / O 上面提到的那些属性参数,都是位于 marred-default.xml 文件中,这些属性参数的调优方式如表所示。
属性名称 | 类型 | 默认值 | 说明 |
---|---|---|---|
mapreduce.task.io.sort.mb | int | 100 | 配置排序map输出时使用的内存缓冲区的大小,默认100MB,实际开发中可以设置大一些 |
mapreduce.map.sort.spill.percent | float | 0.80 | map输出内存缓冲和用来开始磁盘溢出写过程的记录边界索引的阈值,即最大使用环形缓冲内存的阈值。一般默认是80%。也可以直接设置为100% |
mapreduce.task.io.sort.factor | int | 10 | 排序文件时,一次最多合并的流数,实际开发中可将这个值设置为100 |
mapreduce.task.min.num.spills.for.combine | int | 3 | 运行 Combiner 时,所需的最少溢出文件数(如果已指定 Combiner) |
3. Reduce 阶段
- 合理设置 Map 和 Reduce 数: 两个都不能设置太少,也不能设置太多。太少,会导致 task 等待,延长处理时间;太多,会导致 Map 和 Reduce 任务间竞争资源,造成处理超时等错误。
- 设置 Map 和 Reduce 共存: 调整 slowstart.completedmaps 参数,使 Map 运行到定程度后, Reduce 也开始运行,减少 Reduce 的等待时间。
- 规避使用 Reduce : 因为 Reduce 在用于连接数据集的时候将会产生大量的网络消耗。通过将 MapReduce 参数 setNumReduceTasks 设置为 0 来创建一个只有 Map 的作业
- 合理设置 Reduce 端的 buffer : 默认情况下,数据达到一个國值的时候, buffer 中的数据就会写人磁盘,然后 Reduce 会从磁盘中获得所有的数据。也就是说, buffer 和 Reduce 是没有直接关联的,中间多一个写磁盘→读磁盘的过程,既然有这个弊端,那么就可以通过参数来配置,使得 buffer 中的一部分数据可以直接输送到 Reduce ,从而减少 I / O 开销。这样一来,设置 buffer 需要内存,读取数据需要内存, Reduce 计算也要内存,所以要根据作业的运行情况进行调整。
上面提到的属性参数,都是位于 mapped-default.xml 文件中,这些属性参数的调优方式如表所示。
属性名称 | 类型 | 默认值 | 说明 |
---|---|---|---|
mapreduce.job.reduce.slowstart.completedmaps | float | 0.05 | 当 map task 在执行到5%时,就开始为 reduce申请资源,开始执行 reduce操作, reduce可以开始复制map结果数据和做 reduce shuffle操作 |
mapred.job.reduce.input.buffer.percent | float | 0.0 | 在 reduce过程,内存中保存map输出的空间占整个堆空间的比例。如果 reducer 需要的内存较少,可以增加这个值来最小化访问磁盘的次数 |
4. Shuffle 阶段
Shuffle 阶段的调优就是给 Shuffle 过程尽量多地提供内存空间,以防止出现内存溢出现象,可以由参数 mapped.child.java.opts 来设置,任务节点上的内存大小应尽量大。
上面提到的属性参数,都是位于 mapred-site.xml 文件中,这些属性参数的调优方式如表所示。
属性名称 | 类型 | 默认值 | 说明 |
---|---|---|---|
mapped.map.child.java.opts | int | -Xmx200m | 当用户在不设置该值情况下,会以最大1GB jvm heap size 启动 map task,有可能导致内存溢出,所以最简单的做法就是设大参数,一般设置为-Xmx1024m |
mapred.reduce.child.java.opts | int | -Xmx200m | 当用户在不设置该值情况下,会以最大1 GB jvm heap size启动 Reduce task,也有可能导致内存溢出,所以最简单的做法就是设大参数,一般设置为-Xmx1024m |
5. 其他调优属性
除此之外, Mapreduce还有一些基本的资源属性的配置,这些配置的相关参数都位于 mapred-default.xml 文件中,可以合理配置这些属性提高 Mapreduce性能,下表列举了部分调优属性。
属性名称 | 类型 | 默认值 | 说明 |
---|---|---|---|
mapreduce.map.memory.mb | int | 1024 | 一个 Map Task 可使用的资源上限。如果 Map Task 实际使用的资源量超过该值,则会被强制杀死 |
mapreduce.reduce.memory.mb | int | 1024 | 一个 Reduce Task可使用的资源上限。如果 Reduce Task实际使用的资源量超过该值,则会被强制杀死 |
mapreduce.map.cpu.vcores | int | 1 | 每个 Map Task可使用的最多 cpu core数目 |
mapreduce.reduce.cpu.vcores | int | 1 | 每个 Reduce Task可使用的最多 cpu core数目 |
mapreduce.reduce.shuffle.parallelcopies | int | 5 | 每个 reduce 去map中拿数据的并行数 |
mapreduce.map.maxattempts | int | 4 | 每个 Map Task最大重试次数,一旦重试参数超过该值,则认为 Map Task运行失败 |
mapreduce.reduce.maxattempts | int | 4 | 每个 Reduce Task最大重试次数,一旦重试参数超过 Int 该值,则认为 Reduce Task运行失败 |
YARN 调优
1. RM的内存资源配置, 配置的是资源调度相关
ID | 配置 | 说明 |
---|---|---|
RM1 | yarn.scheduler.minimum-allocation-mb | 分配给AM单个容器可申请的最小内存 |
RM2 | yarn.scheduler.maximum-allocation-mb | 分配给AM单个容器可申请的最大内存 |
最小值可以计算一个节点最大Container数量;一旦设置,不可动态改变
2. NM的内存资源配置,配置的是硬件资源相关
ID | 配置 | 说明 |
---|---|---|
NM1 | yarn.nodemanager.resource.memory-mb | 节点最大可用内存 |
NM2 | yarn.nodemanager.vmem-pmem-ratio | 虚拟内存率,默认2.1 |
RM1、RM2的值均不能大于NM1的值 NM1可以计算节点最大最大Container数量,max(Container)=NM1/RM1 一旦设置,不可动态改变
3. AM内存配置相关参数,配置的是任务相关
ID | 配置 | 说明 |
---|---|---|
AM1 | mapreduce.map.memory.mb | 分配给map Container的内存大小 |
AM2 | mapreduce.reduce.memory.mb | 分配给reduce Container的内存大小 |
这两个值应该在RM1和RM2这两个值之间 AM2的值最好为AM1的两倍 这两个值可以在启动时改变
ID | 配置 | 说明 |
---|---|---|
AM3 | mapreduce.map.java.opts | 运行map任务的jvm参数,如-Xmx,-Xms等选项 |
AM4 | mapreduce.reduce.java.opts | 运行reduce任务的jvm参数,如-Xmx,-Xms等选项 |
这两个值应该在AM1和AM2之间
实践
如上图所示,先看最下面褐色部分,
-
AM参数 mapreduce.map.memory.mb=1536MB,表示AM要为map Container申请1536MB资源,但RM实际分配的内存却是2048MB,因为yarn.scheduler.mininum-allocation-mb=1024MB,这定义了RM最小要分配1024MB,1536MB超过了这个值,所以实际分配给AM的值为2048MB(这涉及到了规整化因子)。
-
AM参数 mapreduce.map.java.opts=-Xmx 1024m,表示运行map任务的jvm内存为1024MB,因为map任务要运行在Container里面,所以这个参数的值略微小于mapreduce.map.memory.mb=1536MB这个值。
-
NM参数 yarn.nodemanager.vmem-pmem-radio=2.1,这表示NodeManager可以分配给map/reduce Container 2.1倍的虚拟内存,按照上面的配置,实际分配给map Container容器的虚拟内存大小为2048*2.1=3225.6MB,若实际用到的内存超过这个值,NM就会kill掉这个map Container,任务执行过程就会出现异常。
-
AM参数 mapreduce.reduce.memory.mb=3072MB,表示分配给reduce Container的容器大小为3072MB,而map Container的大小分配的是1536MB,从这也看出,reduce Container容器的大小最好是map Container大小的两倍。
-
NM参数 yarn.nodemanager.resource.mem.mb=24576MB,这个值表示节点分配给NodeManager的可用内存,也就是节点用来执行yarn任务的内存大小。这个值要根据实际服务器内存大小来配置,比如我们hadoop集群机器内存是128GB,我们可以分配其中的80%给yarn,也就是102GB。 上图中RM的两个参数分别1024MB和8192MB,分别表示分配给AM map/reduce Container的最大值和最小值。
尾声
小明根据上面的文档针对自家的集群情况写了一份《调优方案》,得到了领导的嘉奖!
推荐阅读
-
如何调整 Hadoop?阅读本文,你会满意的。
-
趣谈留言队列,搞清楚留言队列到底是什么!-说到消息队列,洪觉大概能猜到人们听到消息队列的反应,大致可以分为以下几类人。 第一类人,懵懵懂懂,刚上大学接触编程,还没用过消息队列,甚至还以为消息队列就是代码里面要新建一个List之类的;第二类人,听过消息队列,了解消息队列,但具体是什么还不是太明白,只知道一说到消息队列,脑海里马上出现了三组词,削峰、异步、解耦;第三类人,用过消息队列,对它有一定了解,但不知道为什么要这样设计,消息队列有什么样的前世今生,是如何演化到现在的模式的?**第四类人,已经对消息队列有了足够的了解,可以阅读本帖作为复习和温习。**你属于哪一类?无论你对消息队列了解多少,读完这篇文章后,我相信你都会有所收获。 什么是消息队列?我们为什么要使用消息队列?真的只是因为它看起来很勉强、很常用吗?当然不是,一项技术的出现往往是为了解决某种痛点,我们就从这个痛点出发,看看消息队列到底是为了解决什么问题而诞生的。 相信大家在工作之前,或者工作中接触单片机的次数会多一点,不管什么业务都一股脑塞进一个系统里,这种情况下接触消息队列的场景会比较少。但随着业务的增长,量上去了,单机系统就很难维护了,也扛不住并发量的增长,就需要把原来的单体应用拆分成多个服务。例如,牛奇网采用分布式架构,将原来的单体系统拆分成用户服务、题库服务、求职服务、论坛服务等,每个分布式节点都有一个集群,保证高可用性。 那虽然在这样的微服务架构下,如果某个核心业务并发量过大,系统就扛不住了。比如淘宝、淘票票、拼多多、京东等电商场景中的支付场景,你在某宝下单并支付后,调用支付服务,完成支付后,还需要更新订单的状态,这个时候就需要调用订单服务,那我们平时也下单,除了简单完成这些操作外,还会给你相应的积分;商家也会收到订单消息,并给您发送旺旺消息,确认订单无误;同时,也会给您发送消息,确认订单无误。确认订单无误;同时您还可以查看您的物流状态;还有系统为了给您推荐更适合您的商品,会根据您的订单做类似的推荐等等,我说的这些都是当我们下单后,肉眼可以感知到系统所做的动作。 **一个支付动作如果还需要调用那么多服务,等他们响应成功,最后再告诉用户你支付成功了,用户在系统中的整个体验会非常糟糕。**设想一下,假设请求服务+处理请求+响应总共需要 50ms,我们上面列出的场景:支付服务、订单服务、积分服务、商家服务、物流服务、推荐服务,总共需要 300ms。
-
Grid++Report 锐浪报表开发常见问题解答集锦-报表设计 问:怎样在设计时打印预览报表? 答:为了及时查看报表的设计效果,Grid++Report 报表设计应用程序提供了四种查看视图:普通视图、页面视图、预览视图与查询视图。通过窗口下边的 Tab 按钮可以在四种视图中任意切换。在预览视图中查看报表的打印预览效果,在查询视图中查看报表的查询显示效果。如果在报表的记录集提供了数据源连接串与查询 SQL,在进入预览视图与查询视图时会利用数据源连接串与查询 SQL 从数据源中自动取数,否则 Grid++Report 将自动生成模拟数据进行模拟打印预览与查询显示。注意:在预览视图与查询视图中看到的报表运行结果有可能与在你程序中的最终运行结果有差异,因为在报表的生成过程中我们可以在程序中对报表的生成行为进行一定的控制。 问:怎样用 Grid++Report 设计交叉表? 答:Grid++Report 没有提供专门实现交叉表的功能,其它的报表构件提供的交叉表功能一般也比较死板和功能有限。利用 Grid++Report 的编程接口可以做出灵活多变,功能丰富的交叉表。示例程序 CrossTab 就是一个实现交叉表的例子程序,认真领会此例子程序,你就可以做出自己想要各种交叉表,并能提取一些共用代码,便于重复使用。 问:怎样设置整个报表的缺省字体? 答:设置报表主对象的字体属性,也就是设置了整个报表的缺省字体。如果改变报表主对象的字体属性,则没有专门的设置字体属性的子对象的字体属性也跟随改变。同样每个报表节与明细网格也有字体属性,他们的字体属性也就是其拥有的子对象的缺省字体。 问:怎样在打印时限制一页的输出行数? 答:设定明细网格的内容行的‘每页行数(RowsPerPage)’属性即可。另外要注意‘调节行高(AdjustRowHeight)’属性值:为真时根据页面的输出高度自动调整行的高度,使整个页面的输出区域充满。为假时按设计时的高度输出行。 问:怎样显示中文大写金额? 答:将对象的“格式(Format)”属性设为 “$$” 及可,可以设置格式的对象有:字段(IGRField)、参数(IGRParameter)、系统变量(IGRSystemVarBox)与综合文字框(IGRMemoBox),其中综合文字框是在报表式上设格式。 问:能否实现自定义纸张与票据打印? 答:Grid++Report 完全支持自定义纸张的打印,只要在报表设定时在页面设置中选定自定义纸张,并指定准确的纸张尺寸。当然要在最终输出时得道合适的打印结果,输出打印机必须支持自定义纸张打印。Windows2000/XP/2003 操作系统上可以在打印机上定义自定义纸张,也可以采用这种方式实现自定义纸张打印。 问:怎样实现 0 值不打印? 答:直接设置格式串就可以,在“数字格式”设置对话框中选定“0 不显示”,就会得到合适的格式串。也可以通过直接录入格式串来指定 0 不显示,但格式串必须符合 Grid++Report 的规定格式。另一种实现办法是在报表获取明细记录数据时,在 BeforePostRecord 事件中将值为零的字段设为空,调用字段的 Clear 方法将字段置为空。 问:怎样实现多栏报表? 答:在明细网格上设‘页栏数(PageColumnCount)’属性值大于 1 即可。通过 Grid++Report 的“页栏输出顺序”还可以指定多栏报表的输出顺序是“先从上到下”还是“先从左到右”。 问:如何实现票据套打? 答:Grid++Report 为实现票据套打做了很多专门的安排:报表设计器提供了页面设计模式,按照设定的纸张尺寸显示设计面板,如果将空白票据的扫描图设为设计背景图,在定位报表内容的输出位置会非常方便。报表部件可以设定打印类别,非套打输出的内容在套打打印模式下就不会输出。 问:Grid++Report 有没有横向分页功能? 答:回答是肯定的,在列的总宽度超过打印页面的输出宽度时,Grid++Report 可以另起新页输出剩余的列,如果左边存在锁定列,锁定列可以在后面的新页中重复输出,这样可以保证关键数据列在每一页都有输出。仔细体会 Grid++Report 提供的多种打印适应策略,选用最合适的方式。Grid++Report 的多种打印适应策略为开发动态报表提供了很好的支持。 问:怎样实现报表本页小计功能? 答:定义一个报表分组,将本分组定义为页分组,在本分组的分组头与分组尾上定义统计。页分组就是在每页产生一个分组项,在每页的上端与下端都会分别显示页分组的分组头与分组尾,页分组不用定义分组依据字段。 报表运行 问:怎样与数据库建立连接? 答:如果在设计报表时指定了数据集的数据源连接串与查询 SQL 语句,Grid++Report 采用拉模式直接从数据源取得报表数据,Grid++Report 利用 OLE DB 从数据源取数,OLE DB 提供了广泛的数据源操作能力。如果 Grid++Report 的数据来源采用推模式,即 Grid++Report 不直接与数据库建立连接,各种编程语言/平台都提供了很好的数据库连接方式,并且易于操作,应用程序在报表主对象(IGridppReport)的 FetchRecord 事件中将数据传入,例子程序提供了各种编程语言填入数据的通用方法,对C++Builder 和 Delphi 还进行了专门的包装,直接关联 TDataSet 对象也可以将 TDataSet 对象中的数据传给报表。 问:打印时能否对打印纸张进行自适应?支持表格的折行打印吗? 答:Grid++Report 在打印时采用多种适应策略,通过设置明细网格(IGRDetailGrid)的‘打印策略(PrintAdaptMethod)’属性指定打印策略。(1)丢弃:按设计时列的宽度输出,超出范围的内容不显示。(2)绕行:按设计时列的宽度输出,如果在当前行不能完整输出,则另起新行进行输出。(3)缩放适应:对所有列的输出宽度进行按比例地缩放,使总宽度等于页面的输出宽度。(4)缩小适应:如果列的总宽度小于页面的输出宽度,对所有列的输出宽度进行按比例地缩小,使总宽度等于页面的输出宽度。(5)横向分页:超范围的列在新页中输出。(6)横向分页并重复锁定列。 问:如何改变缺省打印预览窗口的窗口标题? 答:改变报表主对象的‘标题(Title)’属性即可。 问:利用集合对象的编程接口取子对象的接口引用,但不是自己期望的结果。 答:Grid++Report中所有集合对象的下标索引都是从 1 开始,另按对象的名称查找对象的接口引用时,名称字符是不区分大小写的。 问:怎样在运行时控制报表中各个对象的可见性?即怎样在运行时显示或隐藏对象? 答:在报表主对象(GridppReport)的 SectionFormat 事件中设定相应报表子对象的可见(Visible)属性即可。 问:报表主对象重新载入数据,设计器中为什么没有反映新载入的数据? 答:应调用 IGRDesigner 的 Reload 方法。 问:怎样实现不进入打印预览界面,直接将报表打印出来?
-
F#探险之旅(二):函数式编程(上)-函数式编程范式简介 F#主要支持三种编程范式:函数式编程(Functional Programming,FP)、命令式编程(Imperative Programming)和面向对象(Object-Oriented,OO)的编程。回顾它们的历史,FP是最早的一种范式,第一种FP语言是IPL,产生于1955年,大约在Fortran一年之前。第二种FP语言是Lisp,产生于1958,早于Cobol一年。Fortan和Cobol都是命令式编程语言,它们在科学和商业领域的迅速成功使得命令式编程在30多年的时间里独领风骚。而产生于1970年代的面向对象编程则不断成熟,至今已是最流行的编程范式。有道是“*代有语言出,各领风骚数十年”。 尽管强大的FP语言(SML,Ocaml,Haskell及Clean等)和类FP语言(APL和Lisp是现实世界中最成功的两个)在1950年代就不断发展,FP仍停留在学院派的“象牙塔”里;而命令式编程和面向对象编程则分别凭着在商业领域和企业级应用的需要占据领先。今天,FP的潜力终被认识——它是用来解决更复杂的问题的(当然更简单的问题也不在话下)。 纯粹的FP将程序看作是接受参数并返回值的函数的集合,它不允许有副作用(side effect,即改变了状态),使用递归而不是循环进行迭代。FP中的函数很像数学中的函数,它们都不改变程序的状态。举个简单的例子,一旦将一个值赋给一个标识符,它就不会改变了,函数不改变参数的值,返回值是全新的值。 FP的数学基础使得它很是优雅,FP的程序看起来往往简洁、漂亮。但它无状态和递归的天性使得它在处理很多通用的编程任务时没有其它的编程范式来得方便。但对F#来说这不是问题,它的优势之一就是融合了多种编程范式,允许开发人员按照需要采用最好的范式。 关于FP的更多内容建议阅读一下这篇文章:Why Functional Programming Matters(中文版)。F#中的函数式编程 从现在开始,我将对F#中FP相关的主要语言结构逐一进行介绍。标识符(Identifier) 在F#中,我们通过标识符给值(value)取名字,这样就可以在后面的程序中引用它。通过关键字let定义标识符,如: let x = 42 这看起来像命令式编程语言中的赋值语句,两者有着关键的不同。在纯粹的FP中,一旦值赋给了标识符就不能改变了,这也是把它称为标识符而非变量(variable)的原因。另外,在某些条件下,我们可以重定义标识符;在F#的命令式编程范式下,在某些条件下标识符的值是可以修改的。 标识符也可用于引用函数,在F#中函数本质上也是值。也就是说,F#中没有真正的函数名和参数名的概念,它们都是标识符。定义函数的方式与定义值是类似的,只是会有额外的标识符表示参数: let add x y = x + y 这里共有三个标识符,add表示函数名,x和y表示它的参数。关键字和保留字关键字是指语言中一些标记,它们被编译器保留作特殊之用。在F#中,不能用作标识符或类型的名称(后面会讨论“定义类型”)。它们是: abstract and as asr assert begin class default delegate do donedowncast downto elif else end exception extern false finally forfun function if in inherit inline interface internal land lazy letlor lsr lxor match member mod module mutable namespace new nullof open or override private public rec return sig static structthen to true try type upcast use val void when while with yield 保留字是指当前还不是关键字,但被F#保留做将来之用。可以用它们来定义标识符或类型名称,但编译器会报告一个警告。如果你在意程序与未来版本编译器的兼容性,最好不要使用。它们是: atomic break checked component const constraint constructor continue eager event external fixed functor global include method mixinobject parallel process protected pure sealed trait virtual volatile 文字值(Literals) 文字值表示常数值,在构建计算代码块时很有用,F#提供了丰富的文字值集。与C#类似,这些文字值包括了常见的字符串、字符、布尔值、整型数、浮点数等,在此不再赘述,详细信息请查看F#手册。 与C#一样,F#中的字符串常量表示也有两种方式。一是常规字符串(regular string),其中可包含转义字符;二是逐字字符串(verbatim string),其中的(")被看作是常规的字符,而两个双引号作为双引号的转义表示。下面这个简单的例子演示了常见的文字常量表示: let message = "Hello World"r"n!" // 常规字符串let dir = @"C:"FS"FP" // 逐字字符串let bytes = "bytes"B // byte 数组let xA = 0xFFy // sbyte, 16进制表示let xB = 0o777un // unsigned native-sized integer,8进制表示let print x = printfn "%A" xlet main = print message; print dir; print bytes; print xA; print xB; main Printf函数通过F#的反射机制和.NET的ToString方法来解析“%A”模式,适用于任何类型的值,也可以通过F#中的print_any和print_to_string函数来完成类似的功能。值和函数(Values and Functions) 在F#中函数也是值,F#处理它们的语法也是类似的。 let n = 10let add a b = a + blet addFour = add 4let result = addFour n printfn "result = %i" result 可以看到定义值n和函数add的语法很类似,只不过add还有两个参数。对于add来说a + b的值自动作为其返回值,也就是说在F#中我们不需要显式地为函数定义返回值。对于函数addFour来说,它定义在add的基础上,它只向add传递了一个参数,这样对于不同的参数addFour将返回不同的值。考虑数学中的函数概念,F(x, y) = x + y,G(y) = F(4, y),实际上G(y) = 4 + y,G也是一个函数,它接收一个参数,这个地方是不是很类似?这种只向函数传递部分参数的特性称为函数的柯里化(curried function)。 当然对某些函数来说,传递部分参数是无意义的,此时需要强制提供所有参数,可是将参数括起来,将它们转换为元组(tuple)。下面的例子将不能编译通过: let sub(a, b) = a - blet subFour = sub 4 必须为sub提供两个参数,如sub(4, 5),这样就很像C#中的方法调用了。 对于这两种方式来说,前者具有更高的灵活性,一般可优先考虑。 如果函数的计算过程中需要定义一些中间值,我们应当将这些行进行缩进: let halfWay a b = let dif = b - a let mid = dif / 2 mid + a 需要注意的是,缩进时要用空格而不是Tab,如果你不想每次都按几次空格键,可以在VS中设置,将Tab字符自动转换为空格;虽然缩进的字符数没有限制,但一般建议用4个空格。而且此时一定要用在文件开头添加#light指令。作用域(Scope)作用域是编程语言中的一个重要的概念,它表示在何处可以访问(使用)一个标识符或类型。所有标识符,不管是函数还是值,其作用域都从其声明处开始,结束自其所处的代码块。对于一个处于最顶层的标识符而言,一旦为其赋值,它的值就不能修改或重定义了。标识符在定义之后才能使用,这意味着在定义过程中不能使用自身的值。 let defineMessage = let message = "Help me" print_endline message // error 对于在函数内部定义的标识符,一般而言,它们的作用域会到函数的结束处。 但可使用let关键字重定义它们,有时这会很有用,对于某些函数来说,计算过程涉及多个中间值,因为值是不可修改的,所以我们就需要定义多个标识符,这就要求我们去维护这些标识符的名称,其实是没必要的,这时可以使用重定义标识符。但这并不同于可以修改标识符的值。你甚至可以修改标识符的类型,但F#仍能确保类型安全。所谓类型安全,其基本意义是F#会避免对值的错误操作,比如我们不能像对待字符串那样对待整数。这个跟C#也是类似的。 let changeType = let x = 1 let x = "change me" let x = x + 1 print_string x 在本例的函数中,第一行和第二行都没问题,第三行就有问题了,在重定义x的时候,赋给它的值是x + 1,而x是字符串,与1相加在F#中是非法的。 另外,如果在嵌套函数中重定义标识符就更有趣了。 let printMessages = let message = "fun value" printfn "%s" message; let innerFun = let message = "inner fun value" printfn "%s" message innerFun printfn "%s" message printMessages 打印结果: fun value inner fun valuefun value 最后一次不是inner fun value,因为在innerFun仅仅将值重新绑定而不是赋值,其有效范围仅仅在innerFun内部。递归(Recursion)递归是编程中的一个极为重要的概念,它表示函数通过自身进行定义,亦即在定义处调用自身。在FP中常用于表达命令式编程的循环。很多人认为使用递归表示的算法要比循环更易理解。 使用rec关键字进行递归函数的定义。看下面的计算阶乘的函数: let rec factorial x = match x with | x when x < 0 -> failwith "value must be greater than or equal to 0" | 0 -> 1 | x -> x * factorial(x - 1) 这里使用了模式匹配(F#的一个很棒的特性),其C#版本为: public static long Factorial(int n) { if (n < 0) { throw new ArgumentOutOfRangeException("value must be greater than or equal to 0"); } if (n == 0) { return 1; } return n * Factorial (n - 1); } 递归在解决阶乘、Fibonacci数列这样的问题时尤为适合。但使用的时候要当心,可能会写出不能终止的递归。匿名函数(Anonymous Function) 定义函数的时候F#提供了第二种方式:使用关键字fun。有时我们没必要给函数起名,这种函数就是所谓的匿名函数,有时称为lambda函数,这也是C#3.0的一个新特性。比如有的函数仅仅作为一个参数传给另一个函数,通常就不需要起名。在后面的“列表”一节中你会看到这样的例子。除了fun,我们还可以使用function关键字定义匿名函数,它们的区别在于后者可以使用模式匹配(本文后面将做介绍)特性。看下面的例子: let x = (fun x y -> x + y) 1 2let x1 = (function x -> function y -> x + y) 1 2let x2 = (function (x, y) -> x + y) (1, 2) 我们可优先考虑fun,因为它更为紧凑,在F#类库中你能看到很多这样的例子。 注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。 F#系列随笔索引页面