direct IO submission and completion scalability issues
Subsystems (6)
RFC 1/1 direct IO submission and completion scalability issues
We have been looking into the linux kernel direct IO scalability issues with database workloads. Comments and suggestions on our below experiments are welcome.
In the linux kernel, direct IO requests are not batched at the block layer. i.e, as a new request comes in, the request get directly submitted to the IO controller on the same cpu that the request originates. And the IO completion likely happens on a different cpu which is processing interrupts. This results in cacheline bouncing of some of the hot kernel cachelines (like timers, scsi cmds, slab, sched, etc) and is becoming an important scalability issue as the number of cpus and distance between them increase with multi-core and numa.
In case of the controllers which support RIO/ZIO modes (like some qla2xxx), IO submission path on each cpu also checks if there any completed IO commands in the response queue and triggers softirq on the same cpu to process the completed commands. This results in each logical cpu in the system spending sometime in softirq processing and this causes contentions in spinlocks and other data structures.
Not sure when the IO controllers with multiple request/response queues will be available in the market. In that case we can dedicate each queue pair to group of cpus(/a node) and be done with this problem.
In the absence of such HW today, we were looking into possible solutions for these problemsa and did couple of experiments as part of this.
In the first experiment, we removed the completed IO command processing during IO submission. This will now result in the processing of IO commands only on the cpu receiving interrupts. This will result in more interrupts (as we are not doing any proactive processing) but wanted to see if this is a win over each cpu doing the softirq processing. This gave a 1.36% performance improvement on a x86_64 MP system (total 16 logical cpus) and on two node ia64 platform(2 nodes, 8 cores, 16 threads) we got 1.5% improvement [please look at observation #1 below].
Reference patch for this:
drivers/scsi/qla2xxx/qla_iocb.c +0 -10
@@ -414,11 +414,6 @@ qla2x00_start_scsi(srb_t *sp)WRT_REG_WORD (ISP_REQ_Q_IN (ha, reg), ha->req_ring_index);RD_REG_WORD_RELAXED (ISP_REQ_Q_IN (ha, reg));/* PCI Posting. */ -/* Manage unprocessed RIO/ZIO commands in response queue. */ -if (ha->flags.process_response_queue && - ha->response_ring_ptr->signature !=RESPONSE_PROCESSED ) -qla2x00_process_response_queue (ha); -spin_unlock_irqrestore (&ha->hardware_lock, flags);return (QLA_SUCCESS );
@@ -844,11 +839,6 @@ qla24xx_start_scsi(srb_t *sp)WRT_REG_DWORD (®->req_q_in, ha->req_ring_index);RD_REG_DWORD_RELAXED (®->req_q_in);/* PCI Posting. */ -/* Manage unprocessed RIO/ZIO commands in response queue. */ -if (ha->flags.process_response_queue && - ha->response_ring_ptr->signature !=RESPONSE_PROCESSED ) -qla24xx_process_response_queue (ha); -spin_unlock_irqrestore (&ha->hardware_lock, flags);return QLA_SUCCESS ;
Observation #1: This experiment puts heavy load on the cpu processing interrupts. As such, equal distribution of task load by the scheduler didn't give expected performance improvement(as cpu's with no interrupts race to idle and migrate some tasks during idle balance, leading to some increase in idle time aswell as costs associated with excessive task migration). We tweaked our manual task binding so that cpu's with no interrupts get proportionally more load compared to cpu's which process interrupts and this gave a nice performance boost as mentioned above. Perhaps, we need to make the scheduler load balancing aware of the irq load on that cpu.
Second experiment which we did was migrating the IO submission to the IO completion cpu. Instead of submitting the IO on the same cpu where the request arrived, in this experiment the IO submission gets migrated to the cpu that is processing IO completions(interrupt). This will minimize the access to remote cachelines (that happens in timers, slab, scsi layers). The IO submission request is forwarded to the kblockd thread on the cpu receiving the interrupts. As part of this, we also made kblockd thread on each cpu as the highest priority thread, so that IO gets submitted as soon as possible on the interrupt cpu with out any delay. On x86_64 SMP platform with 16 cores, this resulted in 2% performance improvement and 3.3% improvement on two node ia64 platform.
Quick and dirty prototype patch(not meant for inclusion) for this io migration experiment is appended to this e-mail.
Observation #1 mentioned above is also applicable to this experiment. CPU's processing interrupts will now have to cater IO submission/processing load aswell.
Observation #2: This introduces some migration overhead during IO submission. With the current prototype, every incoming IO request results in an IPI and context switch(to kblockd thread) on the interrupt processing cpu. This issue needs to be addressed and main challenge to address is the efficient mechanism of doing this IO migration(how much batching to do and when to send the migrate request?), so that we don't delay the IO much and at the same point, don't cause much overhead during migration.
Source of the IO migration experiment came from an old experiment done in EL3 days (linux-2.4.21-scsi-affine-queue.patch in EL3 GA release, pointed by Arjan). Arjan pointed out that this patch had some perf issues and was taken out in a later update release of EL3. Given that 2.6 has progressed quite a bit from 2.4 days, wondering if we can answer this challenge easily with today's infrastructure.
Experiment - 1 above can be easily incorporated in to linux kernel(by doing the proactive IO cmd completion processing only on cpu processing the interrupts) . We need to address the scheduler load balancing issue (of taking irq load in to account) though.
Is there a simple and better way to efficiently migrate the IO request(perhaps only for direct IO and also based on IO load -- similar to what is pursued in EL3)? Efficient IO migration will further improve the performance numbers stated above.
io migration prototype(and really dirty) patch follows:
block/ll_rw_blk.c +54 -3
123 lines not on this page · raw message
drivers/ata/libata-core.c +1 -0
@@ -5223,6 +5223,7 @@ irqreturn_t ata_interrupt (int irq, void !(ap->flags & ATA_FLAG_DISABLED)) {struct ata_queued_cmd *qc; + ap->scsi_host->irq_cpu =smp_processor_id (); qc =ata_qc_from_tag (ap, ap->active_tag);if (qc && (!(qc->tf.flags &ATA_TFLAG_POLLING )) && (qc->flags &ATA_QCFLAG_ACTIVE ))
drivers/scsi/qla2xxx/qla_def.h +2 -0
@@ -2294,6 +2294,8 @@ typedef struct scsi_qla_host {uint8_t rscn_in_ptr;uint8_t rscn_out_ptr; +unsigned long last_irq_cpu;/* cpu where we got our last irq */ +/* SNS command interfaces. */ ms_iocb_entry_t *ms_iocb;dma_addr_t ms_iocb_dma;
drivers/scsi/qla2xxx/qla_isr.c +3 -0
@@ -44,6 +44,7 @@ qla2100_intr_handler(int irq, void *dev_return (IRQ_NONE ); } + ha->host->irq_cpu =smp_processor_id (); reg = &ha->iobase->isp; status =0 ;
@@ -121,6 +122,7 @@ qla2300_intr_handler(int irq, void *dev_return (IRQ_NONE ); } + ha->host->irq_cpu =smp_processor_id (); reg = &ha->iobase->isp; status =0 ;
@@ -1437,6 +1439,7 @@ qla24xx_intr_handler(int irq, void *dev_return IRQ_NONE ; } + ha->host->irq_cpu =smp_processor_id (); reg = &ha->iobase->isp24; status =0 ;
drivers/scsi/scsi_scan.c +2 -0
@@ -280,6 +280,8 @@ static struct scsi_device *scsi_alloc_sd } sdev->request_queue->queuedata = sdev; +if (sdev->host) + sdev->request_queue->submit_cpu = &sdev->host->irq_cpu;scsi_adjust_queue_depth (sdev,0 , sdev->host->cmd_per_lun);scsi_sysfs_device_initialize (sdev);
include/linux/blkdev.h +4 -0
@@ -392,6 +392,7 @@ struct request_queueint unplug_thresh;/* After this many requests */ unsigned long unplug_delay;/* After this many jiffies */ struct work_struct unplug_work; +struct work_struct request_fn_work;struct backing_dev_info backing_dev_info;
@@ -400,6 +401,8 @@ struct request_queue* ll_rw_blk doesn't touch it. */ void *queuedata; +int cpu_binding; +int *submit_cpu;/* * queue needs bounce pages for pages above this limit
@@ -853,6 +856,7 @@ static inline void put_dev_sector(Sectorstruct work_struct ;int kblockd_schedule_work (struct work_struct *work); +int kblockd_schedule_work_on_cpu (struct work_struct *work,int cpu);void kblockd_flush (void );#define MODULE_ALIAS_BLOCKDEV (major,minor) \
include/linux/workqueue.h +1 -0
@@ -168,6 +168,7 @@ extern struct workqueue_struct *__createextern void destroy_workqueue (struct workqueue_struct *wq);extern int FASTCALL (queue_work (struct workqueue_struct *wq,struct work_struct *work)); +extern int FASTCALL (queue_work_on_cpu (struct workqueue_struct *wq,struct work_struct *work,int cpu));extern int FASTCALL (queue_delayed_work (struct workqueue_struct *wq,struct delayed_work *work,unsigned long delay));extern int queue_delayed_work_on (int cpu,struct workqueue_struct *wq,struct delayed_work *work,unsigned long delay);
include/scsi/scsi_host.h +1 -0
@@ -635,6 +635,7 @@ struct Scsi_Host {unsigned char n_io_port;unsigned char dma_channel;unsigned int irq; +unsigned int irq_cpu;enum scsi_host_state shost_state;
kernel/workqueue.c +19 -1
@@ -218,6 +218,20 @@ int fastcall queue_work(struct workqueue }EXPORT_SYMBOL_GPL (queue_work); +int fastcall queue_work_on_cpu(struct workqueue_struct *wq, struct work_struct *work, +int cpu) +{ +int ret =0 ; + +if (!test_and_set_bit (WORK_STRUCT_PENDING ,work_data_bits (work))) { +BUG_ON (!list_empty (&work->entry)); +__queue_work (per_cpu_ptr (wq->cpu_wq, cpu), work); + ret =1 ; + } +return ret; +} +EXPORT_SYMBOL_GPL (queue_work_on_cpu); +void delayed_work_timer_fn (unsigned long __data) {struct delayed_work *dwork = (struct delayed_work *)__data;
@@ -351,11 +365,15 @@ static int worker_thread(void *__cwq)DECLARE_WAITQUEUE (wait, current);struct k_sigaction sa;sigset_t blocked; +struct sched_param param = { .sched_priority =MAX_RT_PRIO -1 };if (!cwq->freezeable) current->flags |=PF_NOFREEZE ; -set_user_nice (current,-5 ); +if (!strncmp (cwq->wq->name,"kblockd" ,7 )) +sched_setscheduler (current,SCHED_FIFO , ¶m); +else +set_user_nice (current,-5 );/* Block and flush all signals */ sigfillset (&blocked);
Quoted 3 lines · Suresh B
We have been looking into the linux kernel direct IO scalability issues with database workloads. Comments and suggestions on our below experiments are welcome.
This was on an SMP system? These issues are much more pronounced on a NUMA system. There the locality of the device may be a prime issue.
Quoted 8 lines
In the linux kernel, direct IO requests are not batched at the block layer. i.e, as a new request comes in, the request get directly submitted to the IO controller on the same cpu that the request originates. And the IO completion likely happens on a different cpu which is processing interrupts. This results in cacheline bouncing of some of the hot kernel cachelines (like timers, scsi cmds, slab, sched, etc) and is becoming an important scalability issue as the number of cpus and distance between them increase with multi-core and numa.
Yes. The issue is even worse if the submission comes from a remote node. F.e. If we have a system with a scsi controller on node 2. Now I/O submission on node 1 and completion on node 2. In that case the cacheline has to be transferred across the NUMA interlink.
However, you cannot avoid running the completion on the node where the device sits. The device has all sorts of control structures and if you would handle the completion on node 1 then it would have to transfer lots of cachelines that contain device state to node 1.
I think it is better to leave things as is. Or have the I/O submission be relocated to the node of the device.
Quoted 11 lines
Second experiment which we did was migrating the IO submission to the IO completion cpu. Instead of submitting the IO on the same cpu where the request arrived, in this experiment the IO submission gets migrated to the cpu that is processing IO completions(interrupt). This will minimize the access to remote cachelines (that happens in timers, slab, scsi layers). The IO submission request is forwarded to the kblockd thread on the cpu receiving the interrupts. As part of this, we also made kblockd thread on each cpu as the highest priority thread, so that IO gets submitted as soon as possible on the interrupt cpu with out any delay. On
x86_64SMP platform with 16 cores, this resulted in 2% performance improvement and 3.3% improvement on two node ia64 platform.
I think that is the right approach. This will also help in cases where I/O devices can only be accessed from a certain node (NUMA device address restrictions on some systems may not allow remote cacheline access!)
Quoted 7 lines
Observation #2: This introduces some migration overhead during IO submission. With the current prototype, every incoming IO request results in an IPI and context switch(to kblockd thread) on the interrupt processing cpu. This issue needs to be addressed and main challenge to address is the efficient mechanism of doing this IO migration(how much batching to do and when to send the migrate request?), so that we don't delay the IO much and at the same point, don't cause much overhead during migration.
Right.
Quoted 8 lines · Christoph Lameter
Quoted 3 lines · Suresh B
We have been looking into the linux kernel direct IO scalability issues with database workloads. Comments and suggestions on our below experiments are welcome.
This was on an SMP system? These issues are much more pronounced on a NUMA system. There the locality of the device may be a prime issue.
We are looking into both SMP(multi-core) and NUMA systems.
Quoted 9 lines
Yes. The issue is even worse if the submission comes from a remote node. F.e. If we have a system with a scsi controller on node 2. Now I/O submission on node 1 and completion on node 2. In that case the cacheline has to be transferred across the NUMA interlink.
However, you cannot avoid running the completion on the node where the device sits. The device has all sorts of control structures and if you would handle the completion on node 1 then it would have to transfer lots of cachelines that contain device state to node 1.
If the device is capable of multi queues, then some of the control structures, irqbalance can be done based on how those multi queues are distributed.
Quoted 2 lines
I think it is better to leave things as is. Or have the I/O submission be relocated to the node of the device.
In the absence of specialized controllers, it is best to keep the control structures close to the device node and move the I/O submission to this node.
Quoted 12 lines
Quoted 11 lines
Second experiment which we did was migrating the IO submission to the IO completion cpu. Instead of submitting the IO on the same cpu where the request arrived, in this experiment the IO submission gets migrated to the cpu that is processing IO completions(interrupt). This will minimize the access to remote cachelines (that happens in timers, slab, scsi layers). The IO submission request is forwarded to the kblockd thread on the cpu receiving the interrupts. As part of this, we also made kblockd thread on each cpu as the highest priority thread, so that IO gets submitted as soon as possible on the interrupt cpu with out any delay. On
x86_64SMP platform with 16 cores, this resulted in 2% performance improvement and 3.3% improvement on two node ia64 platform.
Quoted 3 lines
I think that is the right approach. This will also help in cases where I/O devices can only be accessed from a certain node (NUMA device address restrictions on some systems may not allow remote cacheline access!)
Ok, there we have no other choice ;-)
Quoted 9 lines
Quoted 7 lines
Observation #2: This introduces some migration overhead during IO submission. With the current prototype, every incoming IO request results in an IPI and context switch(to kblockd thread) on the interrupt processing cpu. This issue needs to be addressed and main challenge to address is the efficient mechanism of doing this IO migration(how much batching to do and when to send the migrate request?), so that we don't delay the IO much and at the same point, don't cause much overhead during migration.
Right.
So any suggestions for making this clean and acceptable to everyone?
thanks,
suresh
Quoted 11 lines · Suresh B
Quoted 10 lines · Christoph Lameter
Quoted 7 lines · Suresh B
Observation #2: This introduces some migration overhead during IO submission. With the current prototype, every incoming IO request results in an IPI and context switch(to kblockd thread) on the interrupt processing cpu. This issue needs to be addressed and main challenge to address is the efficient mechanism of doing this IO migration(how much batching to do and when to send the migrate request?), so that we don't delay the IO much and at the same point, don't cause much overhead during migration.
Quoted 3 lines
Quoted 1 line
Right.
So any suggestions for making this clean and acceptable to everyone?
It is obviously a good idea to hand over the IO at the point which requires the least number of cachelines to be moved, and I think doing it in the block layer is right. Mostly you have to convince the block and driver maintainers I guess.
The scheduler really should be made interrupt-load aware anyway, so I don't have a problem with changing that; or scheduling kblockd at a higher priority, but I don't know if SCHED_FIFO is a good idea. Couldn't it be done in a softirq instead?
Latency for IO migration could be the most difficult problem to solve really. You don't give much details of the workload, profiles, etc... I hope this is for a real world test? Can the locking be improved in simpler ways first?
Just some random questions...
It looks like the main source of cacheline bouncing you're eliminating is from the initial starting of IO from an empty queue (ie. unplug).
Quoted 1 line
From then on, the submission is driven by completion, right?
Why is the queue allowed to go empty in the first place in an IO critical workload?
Are you loading up each CPU with as many disks as it can possibly handle plus a few more? If so, is that realistic? (I honestly don't know).
You say that you'd like to do this for direct IO only, but if it is more efficient, why not for buffered IO as well? (or is it not more efficient for buffered IO? if not, why?)
AFAIKS, you'd still have significant queue_lock contention from other CPUs inserting requests into the list? What IO scheduler are you using? I assume noop... as a crazy experiment, what happens if you create per-cpu request queues?
Quoted 7 lines · Nick Piggin
Quoted 1 line · Suresh B
So any suggestions for making this clean and acceptable to everyone?
It is obviously a good idea to hand over the IO at the point which requires the least number of cachelines to be moved, and I think doing it in the block layer is right. Mostly you have to convince the block and driver maintainers I guess.
Yes. Implementation is the challenging part I guess.
Quoted 4 lines
The scheduler really should be made interrupt-load aware anyway, so I don't have a problem with changing that; or scheduling kblockd at a higher priority, but I don't know if
SCHED_FIFOis a good idea. Couldn't it be done in a softirq instead?
Yes, softirq context is one way. But just didn't want to penalize the running task by taking away some of its cpu time. With CFS micro accounting, perhaps we can track irq, softirq time and avoid penalizing the running task's cpu time.
Quoted 3 lines
Latency for IO migration could be the most difficult problem to solve really. You don't give much details of the workload, profiles, etc... I hope this is for a real world test?
Improvement numbers quoted are from the OLTP database workload. We can look into other workloads.
Quoted 10 lines
Can the locking be improved in simpler ways first?
Just some random questions...
It looks like the main source of cacheline bouncing you're eliminating is from the initial starting of IO from an empty queue (ie. unplug). From then on, the submission is driven by completion, right?
Why is the queue allowed to go empty in the first place in an IO critical workload?
This workload is using direct IO and there is no batching at the block layer for direct IO. IO is submitted to the HW as it arrives.
Quoted 2 lines
Are you loading up each CPU with as many disks as it can possibly handle plus a few more? If so, is that realistic? (I honestly don't know).
There is 3-4% iowait time in the system. So the cpu's are not 100% busy, but there is quite a bit of direct IO going on.
Quoted 3 lines
You say that you'd like to do this for direct IO only, but if it is more efficient, why not for buffered IO as well? (or is it not more efficient for buffered IO? if not, why?)
It is applicable for both direct IO and buffered IO. But the implementations will differ. For example in buffered IO, we can setup in such a way that the block plug timeout function runs on the IO completion cpu.
Quoted 2 lines
AFAIKS, you'd still have significant
queue_lockcontention from other CPUs inserting requests into the list?
Correct. We have more potential to explore. Current implementation is very elementary.
Quoted 1 line
What IO scheduler are you using? I assume noop...
yes.
Quoted 1 line
as a crazy experiment, what happens if you create per-cpu request queues?
or in other words, each kblockd thread catering multiple request queues (perhaps one for each cpu or one for group of cpu's).
softirq context and each kblockd thread handling multiple request queues will lead to further improvements.
thanks,
suresh
Quoted 16 lines · Suresh B
Quoted 7 lines · Nick Piggin
Quoted 1 line · Suresh B
So any suggestions for making this clean and acceptable to everyone?
It is obviously a good idea to hand over the IO at the point which requires the least number of cachelines to be moved, and I think doing it in the block layer is right. Mostly you have to convince the block and driver maintainers I guess.
Yes. Implementation is the challenging part I guess.
Quoted 4 lines
The scheduler really should be made interrupt-load aware anyway, so I don't have a problem with changing that; or scheduling kblockd at a higher priority, but I don't know if
SCHED_FIFOis a good idea. Couldn't it be done in a softirq instead?
Quoted 4 lines
Yes, softirq context is one way. But just didn't want to penalize the running task by taking away some of its cpu time. With CFS micro accounting, perhaps we can track irq, softirq time and avoid penalizing the running task's cpu time.
But you "penalize" the running task in the completion handler as well anyway. Doing this with a SCHED_FIFO task is sort of like doing interrupt threading which AFAIK has not been accepted (yet).
Quoted 15 lines
Quoted 3 lines
Latency for IO migration could be the most difficult problem to solve really. You don't give much details of the workload, profiles, etc... I hope this is for a real world test?
Improvement numbers quoted are from the OLTP database workload. We can look into other workloads.
Quoted 8 lines
Can the locking be improved in simpler ways first?
Just some random questions...
It looks like the main source of cacheline bouncing you're eliminating is from the initial starting of IO from an empty queue (ie. unplug). From then on, the submission is driven by completion, right?
Quoted 5 lines
Quoted 2 lines
Why is the queue allowed to go empty in the first place in an IO critical workload?
This workload is using direct IO and there is no batching at the block layer for direct IO. IO is submitted to the HW as it arrives.
So you aren't putting concurrent requests into the queue? Sounds like userspace should be improved.
Quoted 6 lines
Quoted 2 lines
Are you loading up each CPU with as many disks as it can possibly handle plus a few more? If so, is that realistic? (I honestly don't know).
There is 3-4% iowait time in the system. So the cpu's are not 100% busy, but there is quite a bit of direct IO going on.
Quoted 7 lines
Quoted 3 lines
You say that you'd like to do this for direct IO only, but if it is more efficient, why not for buffered IO as well? (or is it not more efficient for buffered IO? if not, why?)
It is applicable for both direct IO and buffered IO. But the implementations will differ. For example in buffered IO, we can setup in such a way that the block plug timeout function runs on the IO completion cpu.
It would be nice to be doing that anyway. But unplug via request submission rather than timeout is fairly common in buffered loads too.
Quoted 17 lines
Quoted 2 lines
AFAIKS, you'd still have significant
queue_lockcontention from other CPUs inserting requests into the list?Correct. We have more potential to explore. Current implementation is very elementary.
Quoted 1 line
What IO scheduler are you using? I assume noop...
yes.
Quoted 1 line
as a crazy experiment, what happens if you create per-cpu request queues?
or in other words, each kblockd thread catering multiple request queues (perhaps one for each cpu or one for group of cpu's).
softirq context and each kblockd thread handling multiple request queues will lead to further improvements.
Quoted 8 lines · Nick Piggin
Quoted 4 lines · Suresh B
Yes, softirq context is one way. But just didn't want to penalize the running task by taking away some of its cpu time. With CFS micro accounting, perhaps we can track irq, softirq time and avoid penalizing the running task's cpu time.
But you "penalize" the running task in the completion handler as well anyway.
Yes.
Ingo, in general with CFS micro accounting, we should be able to avoid penalizing the running task by tracking irq/softirq time. Isn't it?
Quoted 2 lines
Doing this with a
SCHED_FIFOtask is sort of like doing interrupt threading which AFAIK has not been accepted (yet).
I am not recommending SCHED_FIFO. I will take a look at softirq infrastructure for this.
Quoted 5 lines
Quoted 2 lines
This workload is using direct IO and there is no batching at the block layer for direct IO. IO is submitted to the HW as it arrives.
So you aren't putting concurrent requests into the queue? Sounds like userspace should be improved.
Nick remember that there are hundreds of disks in this setup and at an instance, there will be max 1 or 2 requests per disk.
Quoted 6 lines
Quoted 3 lines
It is applicable for both direct IO and buffered IO. But the implementations will differ. For example in buffered IO, we can setup in such a way that the block plug timeout function runs on the IO completion cpu.
It would be nice to be doing that anyway. But unplug via request submission rather than timeout is fairly common in buffered loads too.
Ok. Currently the patch handles both direct and buffered IO. While making improvements to this patch I will make sure that both the paths take advantage of this.
thanks,
suresh
Quoted 11 lines · Suresh B
Quoted 7 lines · Nick Piggin
Quoted 3 lines · Suresh B
task by taking away some of its cpu time. With CFS micro accounting, perhaps we can track irq, softirq time and avoid penalizing the running task's cpu time.
But you "penalize" the running task in the completion handler as well anyway.
Yes.
Quoted 8 lines
Ingo, in general with CFS micro accounting, we should be able to avoid penalizing the running task by tracking irq/softirq time. Isn't it?
Quoted 2 lines
Doing this with a
SCHED_FIFOtask is sort of like doing interrupt threading which AFAIK has not been accepted (yet).I am not recommending
SCHED_FIFO. I will take a look at softirq infrastructure for this.
I think that would be a fine way to go.
Quoted 8 lines
Quoted 5 lines
Quoted 2 lines
This workload is using direct IO and there is no batching at the block layer for direct IO. IO is submitted to the HW as it arrives.
So you aren't putting concurrent requests into the queue? Sounds like userspace should be improved.
Nick remember that there are hundreds of disks in this setup and at an instance, there will be max 1 or 2 requests per disk.
Well if there is 2 requests per disk, that's a good thing; you won't need to unplug. If there is only 1, then as well as the plugging cost, the hardeware loses some ability to pipeline things effectively.
I'm not saying the kernel shouldn't be improved in the latter case, but if you're looking for performance, it is nice to ensure you have at least 2 requests. Presumably you're using some pretty well tuned db software though, so I guess this is not always possible.
Do you have stats for these things (queue empty vs not empty events, unplugs, etc)?
Quoted 10 lines
Quoted 6 lines
Quoted 3 lines
It is applicable for both direct IO and buffered IO. But the implementations will differ. For example in buffered IO, we can setup in such a way that the block plug timeout function runs on the IO completion cpu.
It would be nice to be doing that anyway. But unplug via request submission rather than timeout is fairly common in buffered loads too.
Ok. Currently the patch handles both direct and buffered IO. While making improvements to this patch I will make sure that both the paths take advantage of this.
Sounds good!
Quoted 20 lines · Suresh B
Second experiment which we did was migrating the IO submission to the IO completion cpu. Instead of submitting the IO on the same cpu where the request arrived, in this experiment the IO submission gets migrated to the cpu that is processing IO completions(interrupt). This will minimize the access to remote cachelines (that happens in timers, slab, scsi layers). The IO submission request is forwarded to the kblockd thread on the cpu receiving the interrupts. As part of this, we also made kblockd thread on each cpu as the highest priority thread, so that IO gets submitted as soon as possible on the interrupt cpu with out any delay. On
x86_64SMP platform with 16 cores, this resulted in 2% performance improvement and 3.3% improvement on two node ia64 platform.Quick and dirty prototype patch(not meant for inclusion) for this io migration experiment is appended to this e-mail.
Observation #1 mentioned above is also applicable to this experiment. CPU's processing interrupts will now have to cater IO submission/processing load aswell.
Quoted 7 lines
Observation #2: This introduces some migration overhead during IO submission. With the current prototype, every incoming IO request results in an IPI and context switch(to kblockd thread) on the interrupt processing cpu. This issue needs to be addressed and main challenge to address is the efficient mechanism of doing this IO migration(how much batching to do and when to send the migrate request?), so that we don't delay the IO much and at the same point, don't cause much overhead during migration.
Hi guys,
Just had another way we might do this. Migrate the completions out to the submitting CPUs rather than migrate submission into the completing CPU.
I've got a basic patch that passes some stress testing. It seems fairly simple to do at the block layer, and the bulk of the patch involves introducing a scalable smp_call_function for it.
Now it could be optimised more by looking at batching up IPIs or optimising the call function path or even mirating the completion event at a different level...
However, this is a first cut. It actually seems like it might be taking slightly more CPU to process block IO (~0.2%)... however, this is on my dual core system that shares an llc, which means that there are very few cache benefits to the migration, but non-zero overhead. So on multisocket systems hopefully it might get to positive territory.
---
arch/x86/kernel/smp_64.c +93 -0
@@ -321,6 +321,99 @@ void unlock_ipi_call_lock(void)spin_unlock_irq (&call_lock); } +struct call_single_data { +struct list_head list; +void (*func) (void *info); +void *info; +int wait; +}; + +struct call_single_queue { +spinlock_t lock; +struct list_head list; +}; +static DEFINE_PER_CPU(struct call_single_queue, call_single_queue); + +int __cpuinit init_smp_call (void ) +{ +int i; + +for_each_cpu_mask (i, cpu_possible_map) { +spin_lock_init (&per_cpu (call_single_queue, i).lock); +INIT_LIST_HEAD (&per_cpu (call_single_queue, i).list); + } +return 0 ; +} +core_initcall (init_smp_call); + +/* +* this function sends a 'generic call function' IPI to all other CPU +* of the system defined in the mask. +*/ +int smp_call_function_fast (int cpu,void (*func)(void *),void *info, +int wait) +{ +struct call_single_data *data; +struct call_single_queue *dst = &per_cpu (call_single_queue, cpu); +cpumask_t mask =cpumask_of_cpu (cpu); +int ipi; + + data =kmalloc (sizeof (struct call_single_data ),GFP_ATOMIC ); + data->func = func; + data->info = info; + data->wait = wait; + +spin_lock_irq (&dst->lock); + ipi =list_empty (&dst->list); +list_add_tail (&data->list, &dst->list); +spin_unlock_irq (&dst->lock); + +if (ipi) +send_IPI_mask (mask,CALL_FUNCTION_SINGLE_VECTOR ); + +if (wait) { +/* Wait for response */ +while (data->wait) +cpu_relax (); +kfree (data); + } + +return 0 ; +} + +asmlinkage void smp_call_function_fast_interrupt(void) +{ +struct call_single_queue *q; +unsigned long flags; +LIST_HEAD (list); + +ack_APIC_irq (); + + q = &__get_cpu_var (call_single_queue); +spin_lock_irqsave (&q->lock, flags); +list_replace_init (&q->list, &list); +spin_unlock_irqrestore (&q->lock, flags); + +exit_idle (); +irq_enter (); +while (!list_empty (&list)) { +struct call_single_data *data; + + data = list_entry(list.next, struct call_single_data, list); +list_del (&data->list); + + data->func (data->info); +if (data->wait) { +smp_mb (); + data->wait =0 ; + }else { +kfree (data); + } + } +add_pda (irq_call_count,1 ); +irq_exit (); +} +/* * this function sends a 'generic call function' IPI to all other CPU * of the system defined in the mask.
block/blk-core.c +16 -1
@@ -1604,6 +1604,13 @@ static int __end_that_request_first(strureturn 1 ; } +static void blk_done_softirq_other (void *data) +{ +struct request *rq = data; + +blk_complete_request (rq); +} +/* * splice the completion data to a local structure and hand off to * process_completion_queue() to complete the requests
@@ -1622,7 +1629,15 @@ static void blk_done_softirq(struct soft rq = list_entry(local_list.next, struct request, donelist);list_del_init (&rq->donelist); - rq->q->softirq_done_fn (rq); +if (rq->submission_cpu !=smp_processor_id ()) { +/* +* Could batch up IPIs here, but we should measure how +* often blk_done_softirq gets a large batch... +*/ +smp_call_function_fast (rq->submission_cpu, + blk_done_softirq_other, rq,0 ); + }else + rq->q->softirq_done_fn (rq); } }
include/asm-x86/hw_irq_64.h +2 -2
@@ -68,8 +68,7 @@#define ERROR_APIC_VECTOR 0xfe#define RESCHEDULE_VECTOR 0xfd#define CALL_FUNCTION_VECTOR 0xfc -/* fb free - please don't readd KDB here because it's useless -(hint - think what a NMI bit does to a vector) */ +#define CALL_FUNCTION_SINGLE_VECTOR 0xfb#define THERMAL_APIC_VECTOR 0xfa#define THRESHOLD_APIC_VECTOR 0xf9/* f8 free */
@@ -102,6 +101,7 @@ void spurious_interrupt(void);void error_interrupt (void );void reschedule_interrupt (void );void call_function_interrupt (void ); +void call_function_fast_interrupt (void );void irq_move_cleanup_interrupt (void );void invalidate_interrupt0 (void );void invalidate_interrupt1 (void );
include/linux/smp.h +6 -0
@@ -53,6 +53,7 @@ extern void smp_cpus_done(unsigned int m* Call a function on all other processors */ int smp_call_function (void (*func)(void *info),void *info,int retry,int wait); +int smp_call_function_fast (int cpuid,void (*func)(void *info),void *info,int wait);int smp_call_function_single (int cpuid,void (*func) (void *info),void *info,int retry,int wait);
@@ -92,6 +93,11 @@ static inline int up_smp_call_function(v }#define smp_call_function (func, info, retry, wait) \ (up_smp_call_function(func, info)) +static inline int smp_call_function_fast (int cpuid,void (*func)(void *info),void *info,int wait) +{ +return 0 ; +} +#define on_each_cpu (func,info,retry,wait) \ ({ \ local_irq_disable(); \
block/elevator.c +2 -0
@@ -648,6 +648,8 @@ void elv_insert(struct request_queue *q,void __elv_add_request (struct request_queue *q,struct request *rq,int where,int plug) { + rq->submission_cpu =smp_processor_id (); +if (q->ordcolor) rq->cmd_flags |=REQ_ORDERED_COLOR ;
include/linux/blkdev.h +2 -0
@@ -208,6 +208,8 @@ struct request {int ref_count; +int submission_cpu; +/* * when request is used as a packet command carrier */
arch/x86/kernel/entry_64.S +3 -0
@@ -696,6 +696,9 @@ END(invalidate_interrupt\num) ENTRY(call_function_interrupt) apicinterrupt CALL_FUNCTION_VECTOR,smp_call_function_interrupt END(call_function_interrupt) +ENTRY(call_function_fast_interrupt) + apicinterrupt CALL_FUNCTION_SINGLE_VECTOR,smp_call_function_fast_interrupt +END(call_function_fast_interrupt) ENTRY(irq_move_cleanup_interrupt) apicinterrupt IRQ_MOVE_CLEANUP_VECTOR,smp_irq_move_cleanup_interrupt END(irq_move_cleanup_interrupt)
arch/x86/kernel/i8259_64.c +1 -0
@@ -493,6 +493,7 @@ void __init native_init_IRQ(void)/* IPI for generic function call */ set_intr_gate (CALL_FUNCTION_VECTOR , call_function_interrupt); +set_intr_gate (CALL_FUNCTION_SINGLE_VECTOR , call_function_fast_interrupt);/* Low priority IPI to cleanup after moving an irq */ set_intr_gate (IRQ_MOVE_CLEANUP_VECTOR , irq_move_cleanup_interrupt);
include/asm-x86/mach-default/entry_arch.h +1 -0
@@ -13,6 +13,7 @@ BUILD_INTERRUPT(reschedule_interrupt,RESCHEDULE_VECTOR) BUILD_INTERRUPT(invalidate_interrupt,INVALIDATE_TLB_VECTOR) BUILD_INTERRUPT(call_function_interrupt,CALL_FUNCTION_VECTOR) +BUILD_INTERRUPT(call_function_fast_interrupt,CALL_FUNCTION_SINGLE_VECTOR)#endif /*
Hi Nick,
Quoted 2 lines · Nick Piggin
arch/x86/kernel/smp_64.c +2
-0+asmlinkage void smp_call_function_fast_interrupt(void) +{
[snip]
arch/x86/kernel/smp_64.c void unlock_ipi_call_lock(void) · line 408
Quoted 10 lines
arch/x86/kernel/smp_64.c +10
-0+while (!list_empty (&list)) { +struct call_single_data *data; + + data = list_entry(list.next, struct call_single_data, list); +list_del (&data->list); + + data->func (data->info); +if (data->wait) { +smp_mb (); + data->wait =0 ;
Why do we need smp_mb() here (maybe add a comment to keep Andrew/checkpatch happy)?
Pekka
Quoted 8 lines · Pekka Enberg
Hi Nick,
Quoted 2 lines · Nick Piggin
arch/x86/kernel/smp_64.c +2
-0+asmlinkage void smp_call_function_fast_interrupt(void) +{[snip]
Quoted 13 lines
arch/x86/kernel/smp_64.c void unlock_ipi_call_lock(void) · line 408
Quoted 10 lines
arch/x86/kernel/smp_64.c +10
-0+while (!list_empty (&list)) { +struct call_single_data *data; + + data = list_entry(list.next, struct call_single_data, list); +list_del (&data->list); + + data->func (data->info); +if (data->wait) { +smp_mb (); + data->wait =0 ;Why do we need
smp_mb()here (maybe add a comment to keep Andrew/checkpatch happy)?
Yeah, definitely... it's just a really basic RFC, but I should get into the habit of just doing it anyway.
Thanks,
Nick
Quoted 29 lines · Nick Piggin
Quoted 20 lines · Suresh B
Second experiment which we did was migrating the IO submission to the IO completion cpu. Instead of submitting the IO on the same cpu where the request arrived, in this experiment the IO submission gets migrated to the cpu that is processing IO completions(interrupt). This will minimize the access to remote cachelines (that happens in timers, slab, scsi layers). The IO submission request is forwarded to the kblockd thread on the cpu receiving the interrupts. As part of this, we also made kblockd thread on each cpu as the highest priority thread, so that IO gets submitted as soon as possible on the interrupt cpu with out any delay. On
x86_64SMP platform with 16 cores, this resulted in 2% performance improvement and 3.3% improvement on two node ia64 platform.Quick and dirty prototype patch(not meant for inclusion) for this io migration experiment is appended to this e-mail.
Observation #1 mentioned above is also applicable to this experiment. CPU's processing interrupts will now have to cater IO submission/processing load aswell.
Quoted 7 lines
Observation #2: This introduces some migration overhead during IO submission. With the current prototype, every incoming IO request results in an IPI and context switch(to kblockd thread) on the interrupt processing cpu. This issue needs to be addressed and main challenge to address is the efficient mechanism of doing this IO migration(how much batching to do and when to send the migrate request?), so that we don't delay the IO much and at the same point, don't cause much overhead during migration.
Quoted 5 lines
Hi guys,
Just had another way we might do this. Migrate the completions out to the submitting CPUs rather than migrate submission into the completing CPU.
Hi Nick,
When Matthew was describing this work at an LCA presentation (not sure whether you were at that presentation or not), Zach came up with the idea that allowing the submitting application control the CPU that the io completion processing was occurring would be a good approach to try. That is, we submit a "completion cookie" with the bio that indicates where we want completion to run, rather than dictating that completion runs on the submission CPU.
The reasoning is that only the higher level context really knows what is optimal, and that changes from application to application. The "complete on the submission CPU" policy _may_ be more optimal for database workloads, but it is definitely suboptimal for XFS and transaction I/O completion handling because it simply drags a bunch of global filesystem state around between all the CPUs running completions. In that case, we really only want a single CPU to be handling the completions.....
(Zach - please correct me if I've missed anything)
Looking at your patch - if you turn it around so that the "submission CPU" field can be specified as the "completion cpu" then I think the patch will expose the policy knobs needed to do the above. Add the bio -> rq linkage to enable filesystems and DIO to control the completion CPU field and we're almost done.... ;)
Cheers,
Dave.
Signature · 3 lines
Dave Chinner Principal Engineer SGI Australian Software Group
Quoted 10 lines · David Chinner
Hi Nick,
When Matthew was describing this work at an LCA presentation (not sure whether you were at that presentation or not), Zach came up with the idea that allowing the submitting application control the CPU that the io completion processing was occurring would be a good approach to try. That is, we submit a "completion cookie" with the bio that indicates where we want completion to run, rather than dictating that completion runs on the submission CPU.
Quoted 2 lines
The reasoning is that only the higher level context really knows what is optimal, and that changes from application to application.
well.. kinda. One of the really hard parts of the submit/completion stuff is that the slab/slob/slub/slib allocator ends up basically "cycling" memory through the system; there's a sink of free memory on all the submission cpus and a source of free memory on the completion cpu. I don't think applications are capable of working out what is best in this scenario..
Quoted 14 lines · Arjan van de Ven
Quoted 10 lines · David Chinner
Hi Nick,
When Matthew was describing this work at an LCA presentation (not sure whether you were at that presentation or not), Zach came up with the idea that allowing the submitting application control the CPU that the io completion processing was occurring would be a good approach to try. That is, we submit a "completion cookie" with the bio that indicates where we want completion to run, rather than dictating that completion runs on the submission CPU.
Quoted 2 lines
The reasoning is that only the higher level context really knows what is optimal, and that changes from application to application.
Quoted 9 lines
well.. kinda. One of the really hard parts of the submit/completion stuff is that
the slab/slob/slub/slib allocator ends up basically "cycling" memory through the system;
there's a sink of free memory on all the submission cpus and a source of free memory
on the completion cpu. I don't think applications are capable of working out what is
best in this scenario..
Applications as in "anything that calls submit_bio()". i.e, direct I/O, filesystems, etc. i.e. not userspace but in-kernel applications.
In XFS, simultaneous io completion on multiple CPUs can contribute greatly to contention of global structures in XFS. By controlling where completions are delivered, we can greatly reduce this contention, especially on large, mulitpathed devices that deliver interrupts to multiple CPUs that may be far distant from each other. We have all the state and intelligence necessary to control this sort policy decision effectively.....
Cheers,
Dave.
Signature · 3 lines
Dave Chinner Principal Engineer SGI Australian Software Group
Quoted 28 lines · David Chinner
Quoted 14 lines · Arjan van de Ven
Quoted 10 lines · David Chinner
Hi Nick,
When Matthew was describing this work at an LCA presentation (not sure whether you were at that presentation or not), Zach came up with the idea that allowing the submitting application control the CPU that the io completion processing was occurring would be a good approach to try. That is, we submit a "completion cookie" with the bio that indicates where we want completion to run, rather than dictating that completion runs on the submission CPU.
Quoted 2 lines
The reasoning is that only the higher level context really knows what is optimal, and that changes from application to application.
Quoted 9 lines
well.. kinda. One of the really hard parts of the submit/completion stuff is that
the slab/slob/slub/slib allocator ends up basically "cycling" memory through the system;
there's a sink of free memory on all the submission cpus and a source of free memory
on the completion cpu. I don't think applications are capable of working out what is
best in this scenario..Applications as in "anything that calls
submit_bio()". i.e, direct I/O, filesystems, etc. i.e. not userspace but in-kernel applications.
Quoted 6 lines
In XFS, simultaneous io completion on multiple CPUs can contribute greatly to contention of global structures in XFS. By controlling where completions are delivered, we can greatly reduce this contention, especially on large, mulitpathed devices that deliver interrupts to multiple CPUs that may be far distant from each other. We have all the state and intelligence necessary to control this sort policy decision effectively.....
Hi Dave,
Thanks for taking a look at the patch... yes it would be easy to turn this bit of state into a more flexible cookie (eg. complete on submitter; complete on interrupt; complete on CPUx/nodex etc.). Maybe we'll need something that complex... I'm not sure, it would probably need more fine tuning. That said, I just wanted to get this approach out there early for rfc.
I guess both you and Arjan have points. For a _lot_ of things, completing on the same CPU as submitter (whether that is migrating submission as in the original patch in the thread, or migrating completion like I do).
You get better behaviour in the slab and page allocators and locality and cache hotness of memory. For example, I guess in a filesystem / pagecache heavy workload, you have to touch each struct page, buffer head, fs private state, and also often have to wake the thread for completion. Much of this data has just been touched at submit time, so doin this on the same CPU is nice...
I'm surprised that the xfs global state bouncing would outweigh the bouncing of all the per-page/block/bio/request/etc data that gets touched during completion. We'll see.
Quoted 6 lines · Nick Piggin
You get better behaviour in the slab and page allocators and locality and cache hotness of memory. For example, I guess in a filesystem / pagecache heavy workload, you have to touch each struct page, buffer head, fs private state, and also often have to wake the thread for completion. Much of this data has just been touched at submit time, so doin this on the same CPU is nice...
[....]
Quoted 3 lines
I'm surprised that the xfs global state bouncing would outweigh the bouncing of all the per-page/block/bio/request/etc data that gets touched during completion. We'll see.
per-page/block.bio/request/etc is local to a single I/O. the only penalty is a cacheline bounce for each of the structures from one CPU to another. That is, there is no global state modified by these completions.
The real issue is metadata. The transaction log I/O completion funnels through a state machine protected by a single lock, which means completions on different CPUs pulls that lock to all completion CPUs. Given that the same lock is used during transaction completion for other state transitions (in task context, not intr), the more cpus active at once touches, the worse the problem gets.
Then there's metadata I/O completion, which funnels through a larger set of global locks in the transaction subsystem (e.g. the active item list lock, the log reservation locks, the log state lock, etc) which once again means the more CPUs we have delivering I/O completions, the worse the problem gets.
Cheers,
Dave.
Signature · 3 lines
Dave Chinner Principal Engineer SGI Australian Software Group
Quoted 10 lines · David Chinner
Quoted 6 lines · Nick Piggin
You get better behaviour in the slab and page allocators and locality and cache hotness of memory. For example, I guess in a filesystem / pagecache heavy workload, you have to touch each struct page, buffer head, fs private state, and also often have to wake the thread for completion. Much of this data has just been touched at submit time, so doin this on the same CPU is nice...
[....]
Quoted 8 lines
Quoted 3 lines
I'm surprised that the xfs global state bouncing would outweigh the bouncing of all the per-page/block/bio/request/etc data that gets touched during completion. We'll see.
per-page/block.bio/request/etc is local to a single I/O. the only penalty is a cacheline bounce for each of the structures from one CPU to another. That is, there is no global state modified by these completions.
Yeah, but it is going from _all_ submitting CPUs to the one completing CPU. So you could bottleneck the interconnect at the completing CPU just as much as if you had cachelines being pulled the other way (ie. many CPUs trying to pull in a global cacheline).
Quoted 6 lines
The real issue is metadata. The transaction log I/O completion funnels through a state machine protected by a single lock, which means completions on different CPUs pulls that lock to all completion CPUs. Given that the same lock is used during transaction completion for other state transitions (in task context, not intr), the more cpus active at once touches, the worse the problem gets.
OK, once you add locking (and not simply cacheline contention), then the problem gets harder I agree. But I think that if the submitting side takes the same locks as log completion (eg. maybe for starting a new transaction), then it is not going to be a clear win either way, and you'd have to measure it in the end.
[ ugh, still jet lagged. ]
Quoted 19 lines
Hi Nick,
When Matthew was describing this work at an LCA presentation (not sure whether you were at that presentation or not), Zach came up with the idea that allowing the submitting application control the CPU that the io completion processing was occurring would be a good approach to try. That is, we submit a "completion cookie" with the bio that indicates where we want completion to run, rather than dictating that completion runs on the submission CPU.
The reasoning is that only the higher level context really knows what is optimal, and that changes from application to application. The "complete on the submission CPU" policy _may_ be more optimal for database workloads, but it is definitely suboptimal for XFS and transaction I/O completion handling because it simply drags a bunch of global filesystem state around between all the CPUs running completions. In that case, we really only want a single CPU to be handling the completions.....
Quoted 1 line
(Zach - please correct me if I've missed anything)
Yeah, I think Nick's patch (and Jens' approach, presumably) is just the sort of thing we were hoping for when discussing this during Matthew's talk.
I was imagining the patch a little bit differently (per-cpu tasks, do a wake_up from the driver instead of cpu nr testing up in blk, work queues, whatever), but we know how to iron out these kinds of details ;).
Quoted 4 lines
Looking at your patch - if you turn it around so that the "submission CPU" field can be specified as the "completion cpu" then I think the patch will expose the policy knobs needed to do the above.
Yeah, that seems pretty straight forward.
We might need some logic for noticing that the desired cpu has been hot-plugged away while the IO was in flight, it occurs to me.
- z
Quoted 21 lines · Zach Brown
[ ugh, still jet lagged. ]
Quoted 19 lines
Hi Nick,
When Matthew was describing this work at an LCA presentation (not sure whether you were at that presentation or not), Zach came up with the idea that allowing the submitting application control the CPU that the io completion processing was occurring would be a good approach to try. That is, we submit a "completion cookie" with the bio that indicates where we want completion to run, rather than dictating that completion runs on the submission CPU.
The reasoning is that only the higher level context really knows what is optimal, and that changes from application to application. The "complete on the submission CPU" policy _may_ be more optimal for database workloads, but it is definitely suboptimal for XFS and transaction I/O completion handling because it simply drags a bunch of global filesystem state around between all the CPUs running completions. In that case, we really only want a single CPU to be handling the completions.....
Quoted 8 lines
Quoted 1 line
(Zach - please correct me if I've missed anything)
Yeah, I think Nick's patch (and Jens' approach, presumably) is just the sort of thing we were hoping for when discussing this during Matthew's talk.
I was imagining the patch a little bit differently (per-cpu tasks, do a
wake_upfrom the driver instead of cpu nr testing up in blk, work queues, whatever), but we know how to iron out these kinds of details ;).
per-cpu tasks/wq's might be better, it's a little awkward to jump through hoops
Quoted 9 lines
Quoted 4 lines
Looking at your patch - if you turn it around so that the "submission CPU" field can be specified as the "completion cpu" then I think the patch will expose the policy knobs needed to do the above.
Yeah, that seems pretty straight forward.
We might need some logic for noticing that the desired cpu has been hot-plugged away while the IO was in flight, it occurs to me.
the softirq completion stuff already handles cpus going away, at least with my patch that stuff works fine (with a dead flag added).
Signature · 1 line
Jens Axboe
Quoted 7 lines · Jens Axboe
Quoted 3 lines
I was imagining the patch a little bit differently (per-cpu tasks, do a
wake_upfrom the driver instead of cpu nr testing up in blk, work queues, whatever), but we know how to iron out these kinds of details ;).per-cpu tasks/wq's might be better, it's a little awkward to jump through hoops
one caveat btw; when the multiqueue storage hw becomes available for Linux, we need to figure out how to deal with the preference thing; since there honoring a "non-logical" preference would be quite expensive (it means
you can't make the local submit queues lockless etc etc), so before we go down the road of having widespread APIs for this stuff.. we need to make sure we're not going to do something that's going to be really stupid 6 to 18 months down the road.
Quoted 5 lines · Arjan van de Ven
Quoted 4 lines · Jens Axboe
Quoted 3 lines
I was imagining the patch a little bit differently (per-cpu tasks, do a
wake_upfrom the driver instead of cpu nr testing up in blk, work queues, whatever), but we know how to iron out these kinds of details ;).
Quoted 7 lines
Quoted 3 lines
per-cpu tasks/wq's might be better, it's a little awkward to jump through hoops
one caveat btw; when the multiqueue storage hw becomes available for Linux, we need to figure out how to deal with the preference thing; since there honoring a "non-logical" preference would be quite expensive (it means
non-local?
Quoted 4 lines
you can't make the local submit queues lockless etc etc), so before we go down the road of having widespread APIs for this stuff.. we need to make sure we're not going to do something that's going to be really stupid 6 to 18 months down the road.
As far as I'm concerned, so far this is just playing around with affinity (and to some extents taking it too far, on purpose). For instance, my current patch can move submissions and completions independently, with a set mask or by 'binding' a request to a CPU. Most of that doesn't make sense. 'complete on the same CPU, if possible' makes sense and would fit fine with multi-queue hw.
Moving submissions at the block layer to a defined set of CPUs is a bit silly imho, it's pretty costly and it's a lot more sane simply bind the submitters instead. So if you can set irq affinity, then just make the submitters follow that.
Signature · 1 line
Jens Axboe
Quoted 43 lines · Nick Piggin
Quoted 20 lines · Suresh B
Second experiment which we did was migrating the IO submission to the IO completion cpu. Instead of submitting the IO on the same cpu where the request arrived, in this experiment the IO submission gets migrated to the cpu that is processing IO completions(interrupt). This will minimize the access to remote cachelines (that happens in timers, slab, scsi layers). The IO submission request is forwarded to the kblockd thread on the cpu receiving the interrupts. As part of this, we also made kblockd thread on each cpu as the highest priority thread, so that IO gets submitted as soon as possible on the interrupt cpu with out any delay. On
x86_64SMP platform with 16 cores, this resulted in 2% performance improvement and 3.3% improvement on two node ia64 platform.Quick and dirty prototype patch(not meant for inclusion) for this io migration experiment is appended to this e-mail.
Observation #1 mentioned above is also applicable to this experiment. CPU's processing interrupts will now have to cater IO submission/processing load aswell.
Quoted 7 lines
Observation #2: This introduces some migration overhead during IO submission. With the current prototype, every incoming IO request results in an IPI and context switch(to kblockd thread) on the interrupt processing cpu. This issue needs to be addressed and main challenge to address is the efficient mechanism of doing this IO migration(how much batching to do and when to send the migrate request?), so that we don't delay the IO much and at the same point, don't cause much overhead during migration.
Hi guys,
Just had another way we might do this. Migrate the completions out to the submitting CPUs rather than migrate submission into the completing CPU.
I've got a basic patch that passes some stress testing. It seems fairly simple to do at the block layer, and the bulk of the patch involves introducing a scalable
smp_call_functionfor it.Now it could be optimised more by looking at batching up IPIs or optimising the call function path or even mirating the completion event at a different level...
Quoted 5 lines
However, this is a first cut. It actually seems like it might be taking slightly more CPU to process block IO (~0.2%)... however, this is on my dual core system that shares an llc, which means that there are very few cache benefits to the migration, but non-zero overhead. So on multisocket systems hopefully it might get to positive territory.
That's pretty funny, I did pretty much the exact same thing last week! The primary difference between yours and mine is that I used a more private interface to signal a softirq raise on another CPU, instead of allocating call data and exposing a generic interface. That put the locking in blk-core instead, turning blk_cpu_done into a structure with a lock and list_head instead of just being a list head, and intercepted at blk_complete_request() time instead of waiting for an already raised softirq on that CPU.
Didn't get around to any performance testing yet, though. Will try and clean it up a bit and do that.
Signature · 1 line
Jens Axboe
Quoted 17 lines · Jens Axboe
Quoted 16 lines · Nick Piggin
On Fri, Jul 27, 2007 at 06:21:28PM -0700, Suresh B wrote:
Hi guys,
Just had another way we might do this. Migrate the completions out to the submitting CPUs rather than migrate submission into the completing CPU.
I've got a basic patch that passes some stress testing. It seems fairly simple to do at the block layer, and the bulk of the patch involves introducing a scalable
smp_call_functionfor it.Now it could be optimised more by looking at batching up IPIs or optimising the call function path or even mirating the completion event at a different level...
Quoted 7 lines
Quoted 5 lines
However, this is a first cut. It actually seems like it might be taking slightly more CPU to process block IO (~0.2%)... however, this is on my dual core system that shares an llc, which means that there are very few cache benefits to the migration, but non-zero overhead. So on multisocket systems hopefully it might get to positive territory.
That's pretty funny, I did pretty much the exact same thing last week!
Oh nice ;)
Quoted 7 lines
The primary difference between yours and mine is that I used a more private interface to signal a softirq raise on another CPU, instead of allocating call data and exposing a generic interface. That put the locking in blk-core instead, turning
blk_cpu_doneinto a structure with a lock andlist_headinstead of just being a list head, and intercepted atblk_complete_request()time instead of waiting for an already raised softirq on that CPU.
Yeah I was looking at that... didn't really want to add the spinlock overhead to the non-migration case. Anyway, I guess that sort of fine implementation details is going to have to be sorted out with results.
Quoted 37 lines · Nick Piggin
Quoted 17 lines · Jens Axboe
Quoted 16 lines · Nick Piggin
On Fri, Jul 27, 2007 at 06:21:28PM -0700, Suresh B wrote:
Hi guys,
Just had another way we might do this. Migrate the completions out to the submitting CPUs rather than migrate submission into the completing CPU.
I've got a basic patch that passes some stress testing. It seems fairly simple to do at the block layer, and the bulk of the patch involves introducing a scalable
smp_call_functionfor it.Now it could be optimised more by looking at batching up IPIs or optimising the call function path or even mirating the completion event at a different level...
Quoted 7 lines
Quoted 5 lines
However, this is a first cut. It actually seems like it might be taking slightly more CPU to process block IO (~0.2%)... however, this is on my dual core system that shares an llc, which means that there are very few cache benefits to the migration, but non-zero overhead. So on multisocket systems hopefully it might get to positive territory.
That's pretty funny, I did pretty much the exact same thing last week!
Oh nice ;)
Quoted 7 lines
The primary difference between yours and mine is that I used a more private interface to signal a softirq raise on another CPU, instead of allocating call data and exposing a generic interface. That put the locking in blk-core instead, turning
blk_cpu_doneinto a structure with a lock andlist_headinstead of just being a list head, and intercepted atblk_complete_request()time instead of waiting for an already raised softirq on that CPU.
Quoted 4 lines
Yeah I was looking at that... didn't really want to add the spinlock overhead to the non-migration case. Anyway, I guess that sort of fine implementation details is going to have to be sorted out with results.
As Andi mentions, we can look into making that lockless. For the initial implementation I didn't really care, just wanted something to play with that would nicely allow me to control both the submit and complete side of the affinity issue.
Signature · 1 line
Jens Axboe
James Bottomley
maintainerQuoted 4 lines · Jens Axboe
As Andi mentions, we can look into making that lockless. For the initial implementation I didn't really care, just wanted something to play with that would nicely allow me to control both the submit and complete side of the affinity issue.
Sorry, late to the party ... it went to my steeleye address, not my current one.
Could you try re-running the tests with a low queue depth (say around 8) and the card interrupt bound to a single CPU.
The reason for asking you to do this is that it should emulate almost precisely what you're looking for: The submit path will be picked up in the SCSI softirq where the queue gets run, so you should find that all submit and returns happen on a single CPU, so everything gets cache hot there.
James
p.s. if everyone could also update my email address to the hansenpartnership one, the people at steeleye who monitor my old email account would be grateful.
arch/x86/kernel/smp_64.c void unlock_ipi_call_lock(void) · line 395
Quoted 4 lines
arch/x86/kernel/smp_64.c +4
-0+ q = &__get_cpu_var (call_single_queue); +spin_lock_irqsave (&q->lock, flags); +list_replace_init (&q->list, &list); +spin_unlock_irqrestore (&q->lock, flags);
I think you could do that lockless if you use a similar data structure as netchannels (essentially a fixed size single buffer queue with atomic exchange of the first/last pointers) and not using a list. That would avoid at least one bounce for the lock and likely another one for the list manipulation.
Also the right way would be to not add a second mechanism for this, but fix the standard smp_call_function_single() to support it.
-Andi
Quoted 5 lines · Nick Piggin
Hi guys,
Just had another way we might do this. Migrate the completions out to the submitting CPUs rather than migrate submission into the completing CPU.
Hi Nick, This was the first experiment I tried on a quad core four package SMP platform. And it didn't show much improvement in my prototype(my protoype was migrating the softirq to the kblockd context of the submitting CPU).
In the OLTP workload, quite a bit of activity happens below the block layer and by the time we come to softirq, some damage is done in slab, scsi cmds, timers etc. Last year OLS paper
(http://ols.108.redhat.com/2007/Reprints/gough-Reprint.pdf)
shows different cache lines that are contended in the kernel for the OLTP workload.
Softirq migration should atleast reduce the cacheline contention that happens in sched and AIO layers. I didn't spend much time why my softirq migration patch didn't help much (as I was behind bigger birds of migrating IO submission to completion CPU at that time). If this solution has less side-effects and easily acceptable, then we can analyze the softirq migration patch further and findout the potential.
While there is some potential with the softirq migration, full potential can be exploited by making the IO submission and completion on the same CPU.
thanks,
suresh